【大白话说Java面试题 第85题】【Mysql篇】第15题:MySQL 的事务中,幻读是怎么解决的?

📌 PDF :大白话说Java面试题 --- 03-Mysql篇

第15题:MySQL 的事务中,幻读是怎么解决的

📚 回答:

  • 核心考点
    大厂面试要求深入理解MVCC与Next-Key Lock的区别快照读 vs 当前读的不同处理方式Next-Key Lock的加锁范围 ,以及RR级别下幻读是否100%解决。面试官常追问:"RR级别下能完全避免幻读吗?"、"Serializable隔离级别怎么解决幻读?"
1. 幻读的定义

幻读是指:在同一事务中,相同的查询条件 在不同时间点执行,返回的结果集行数不一致

典型场景

sql 复制代码
-- 事务 A
SELECT * FROM users WHERE age = 20;  -- 返回 10 条

-- 事务 B 插入了一条 age=20 的新记录
INSERT INTO users (age) VALUES (20);

-- 事务 A 再次查询
SELECT * FROM users WHERE age = 20;  -- 返回 11 条(幻读)

幻读 vs 不可重复读

问题 定义 示例
不可重复读 同一行数据,两次读取的内容不一致 第一次读到name='张三',第二次变成name='李四'
幻读 两次查询返回的行数不一致 第一次10行,第二次11行(新增)

注意 :MySQL的InnoDB在RR级别下,MVCC已经解决了快照读 的幻读,但当前读仍需Next-Key Lock解决。

2. 解决方案概览
读类型 解决方案 原理
快照读(SELECT) MVCC 读取Undo日志中的历史版本,不受新插入数据影响
当前读(SELECT FOR UPDATE/UPDATE/DELETE) Next-Key Lock 锁定记录+间隙,防止其他事务插入

为什么需要两种方案?

  • MVCC:无锁并发,性能好,但只能用于快照读
  • Next-Key Lock:阻塞其他事务,保证当前读数据一致性,有锁代价
3. MVCC如何解决快照读的幻读

3.1 MVCC核心组件

组件 作用 存储位置
Undo Log版本链 记录每次修改的历史版本,通过roll_pointer串联 Undo表空间
ReadView 判断事务可见性的快照 事务执行快照读时生成
trx_id 最近修改该行的事务ID 每行记录头信息
roll_pointer 指向上一个版本的指针 每行记录头信息

3.2 ReadView判断规则

cpp 复制代码
// ReadView核心字段
m_ids        // 当前未提交的事务ID列表(活跃事务)
min_trx_id   // m_ids中的最小值
max_trx_id   // 系统预分配的下一个事务ID(即当前最大事务ID+1)
creator_trx_id // 创建该ReadView的事务ID

// 可见性判断规则(伪代码)
bool isVisible(trx_id) {
    if (trx_id == creator_trx_id) return true;          // 当前事务修改
    if (trx_id < min_trx_id) return true;                // 已提交
    if (trx_id >= max_trx_id) return false;              // 未来事务
    if (trx_id is not in m_ids) return true;             // 已提交
    return false;                                         // 未提交
}

3.3 MVCC解决快照读幻读示例

sql 复制代码
-- 事务 A(开启时间早)
START TRANSACTION;
SELECT * FROM users WHERE age = 20;  -- 生成ReadView_A,返回10行

-- 事务 B(在事务A之后,提交前)
START TRANSACTION;
INSERT INTO users (age) VALUES (20);
COMMIT;

-- 事务 A 再次快照读
SELECT * FROM users WHERE age = 20;  -- 仍用ReadView_A,新插入的行不可见,仍返回10行

关键:事务A的第二次快照读仍使用第一次生成的ReadView,因此看不到事务B插入的新行。

4. Next-Key Lock解决当前读的幻读

4.1 什么是Next-Key Lock

Next-Key Lock = Record Lock(记录锁) + Gap Lock(间隙锁)

锁类型 锁范围 作用
Record Lock 锁定具体索引记录 防止其他事务修改/删除该记录
Gap Lock 锁定索引记录之间的间隙 防止其他事务在该间隙插入新记录
Next-Key Lock Record Lock + Gap Lock 防止幻读(新记录插入)

4.2 Gap Lock的锁定范围

sql 复制代码
-- 假设users表有id索引:1, 3, 5, 7, 9
-- 查询条件:WHERE id = 5 且使用范围锁定

Next-Key Lock锁定范围:(3, 5] 和 (5, 7]
-- 即锁定了3-7之间的所有间隙

间隙锁定规则

  • 锁定的间隙是开区间(不包含边界值,除非是Record Lock)
  • 间隙锁之间不互斥(多个事务可以锁同一个间隙)
  • 间隙锁只阻止插入 ,不阻止

4.3 Next-Key Lock解决当前读幻读示例

sql 复制代码
-- 事务 A(当前读)
START TRANSACTION;
SELECT * FROM users WHERE age = 20 FOR UPDATE;  
-- 获取 Next-Key Lock,锁定 age=20 的记录及对应间隙

-- 事务 B(试图插入)
START TRANSACTION;
INSERT INTO users (age) VALUES (20);  
-- 阻塞,因为事务 A 锁定了该间隙

-- 事务 A 再次查询
SELECT * FROM users WHERE age = 20 FOR UPDATE;  
-- 返回行数不变,无幻读
5. Next-Key Lock在不同查询条件下的加锁范围
查询条件 锁类型 加锁范围 示例(索引值:1,3,5,7,9)
唯一索引等值查询(命中) Record Lock 仅锁定该记录 WHERE id=5 → 锁5
唯一索引等值查询(未命中) Gap Lock 锁定命中间隙 WHERE id=6 → 锁(5,7)
普通索引等值查询(命中) Next-Key Lock 锁记录+前间隙 WHERE age=5 → 锁(3,5]、(5,7)
普通索引等值查询(未命中) Gap Lock 锁定命中间隙 WHERE age=6 → 锁(5,7)
范围查询 Next-Key Lock 锁范围内的所有间隙+记录 WHERE id > 5 → 锁(5,+∞)

关键规则(InnoDB加锁原则):

  1. 唯一索引等值命中 → Record Lock(退化为行锁)
  2. 唯一索引等值未命中 → Gap Lock(锁定间隙)
  3. 普通索引 → Next-Key Lock(无法退化,因为可能有重复值)
  4. 范围查询 → Next-Key Lock,锁到范围结束
  5. 无索引条件 → 全表锁(所有间隙+记录),高并发风险

示例分析

sql 复制代码
-- 表结构
CREATE TABLE t (id INT PRIMARY KEY, age INT, INDEX idx_age(age));
-- 已有数据:age: 1, 3, 5, 7, 9

-- 查询1:唯一索引命中
SELECT * FROM t WHERE id = 5 FOR UPDATE;
-- 锁:id=5的Record Lock(不锁间隙)

-- 查询2:普通索引命中
SELECT * FROM t WHERE age = 5 FOR UPDATE;
-- 锁:Next-Key Lock覆盖(3,5]和(5,7)
-- 不能插入age=4,5,6
-- 但可以插入age=2,8

-- 查询3:范围查询
SELECT * FROM t WHERE age > 5 FOR UPDATE;
-- 锁:所有age>5的Next-Key Lock,直到正无穷
-- 不能插入age=6,7,8,9,10...
6. RR级别下幻读能100%解决吗?

结论 :InnoDB RR级别能100%防止幻读(官方定义)。

官方保障

  • 快照读:MVCC保证同一事务中的SELECT返回一致快照
  • 当前读:Next-Key Lock阻止其他事务插入/删除符合条件的记录

边界情况(面试陷阱)

sql 复制代码
-- 事务 A
SELECT * FROM users WHERE age = 20;  -- 快照读,返回10行
-- 事务 B 插入一条age=20并提交

-- 事务 A 执行当前读
SELECT * FROM users WHERE age = 20 FOR UPDATE;  -- 返回11行(幻读?)

结论 :这不是幻读,因为事务A通过不同读类型 (快照读→当前读)获取了不同的数据视图。幻读的定义要求相同的查询类型。如果事务A第一次就用当前读,第二次也用当前读,不会有幻读。

7. 不同隔离级别下幻读的处理
隔离级别 是否有幻读 解决方案 备注
READ UNCOMMITTED ✅ 有 脏读+幻读
READ COMMITTED ✅ 有 有不可重复读+幻读
REPEATABLE READ ❌ 无 MVCC(快照读)+ Next-Key Lock(当前读) MySQL默认级别
SERIALIZABLE ❌ 无 所有SELECT隐式加锁(LOCK IN SHARE MODE 性能极差,实际不用

RC vs RR区别

  • RC级别没有Gap Lock,所以当前读无法防止幻读
  • RC级别快照读每次都生成新ReadView,所以快照读也无法防止幻读
8. 实战案例分析

案例1:秒杀系统防止超卖

sql 复制代码
-- 商品表
CREATE TABLE product (
    id INT PRIMARY KEY,
    stock INT,
    INDEX idx_stock(stock)
);

-- 减库存(避免幻读:防止减到负数)
START TRANSACTION;
-- 当前读锁定stock范围
SELECT stock FROM product WHERE stock >= 1 FOR UPDATE;
-- 检查后执行减库存
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;

案例2:订单号生成防止重复

sql 复制代码
-- 使用间隙锁防止幻读
START TRANSACTION;
-- 锁定order_no在(100, 200]范围的间隙
SELECT * FROM orders WHERE order_no BETWEEN 100 AND 200 FOR UPDATE;
-- 在此范围内生成新订单号,不会被其他事务插入重复
INSERT INTO orders (order_no) VALUES (150);
COMMIT;
9. 总结对比表
对比维度 MVCC Next-Key Lock
解决的问题 快照读的幻读+不可重复读 当前读的幻读
核心机制 Undo版本链 + ReadView Record Lock + Gap Lock
是否加锁 无锁(非阻塞) 有锁(阻塞其他事务)
隔离级别依赖 RR级别及以上 RR级别及以上
性能开销 中(间隙锁可能扩大锁范围)
适用场景 普通查询 SELECT FOR UPDATEUPDATEDELETE

💡 面试官想要的满分总结

"MySQL通过两种机制解决幻读,取决于读类型:
快照读(普通SELECT) :通过MVCC解决。事务第一次快照读生成ReadView,后续快照读复用该ReadView,通过Undo版本链判断可见性,新提交事务插入的数据不可见,保证了无幻读。
当前读(SELECT FOR UPDATE/UPDATE/DELETE) :通过Next-Key Lock解决。Next-Key Lock = Record Lock + Gap Lock,不仅锁定现有记录,还锁定记录之间的间隙,阻止其他事务在间隙中插入新记录,从而防止幻读。
关键区别

  • MVCC是乐观锁方案:无锁并发,性能好

  • Next-Key Lock是悲观锁方案:加锁阻塞,保证当前读数据一致
    注意点

  • RR级别下,MVCC和Next-Key Lock共同保证100%无幻读

  • 唯一索引等值查询时,Next-Key Lock退化为Record Lock

  • 无索引时,Next-Key Lock会锁全表(所有间隙+记录)
    一句话:快照读的幻读靠MVCC,当前读的幻读靠Next-Key Lock(Record Lock+Gap Lock),两者在RR级别下联手彻底消灭幻读。"


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
瑞雪兆丰年兮1 小时前
[从0开始学Java|第十六、十七天]项目阶段(拼图小游戏)
java·开发语言
清水白石0081 小时前
Python 变量的本质:从“盒子思维”到“引用思维”,彻底理解赋值到底发生了什么
java·python·ajax
yoothey1 小时前
MySQL 索引小白面试详解
数据库·mysql
Solis程序员1 小时前
TreeMap 核心原理与实战
java·数据结构·算法
yaoxin5211231 小时前
423. Java 日期时间 API - DayOfWeek 和 Month 枚举
开发语言·python
秋雨梧桐叶落莳1 小时前
iOS——抽屉视图详解
开发语言·macos·ui·ios·objective-c·cocoa
郝学胜-神的一滴1 小时前
Qt 高级开发 016:半内存管理机制
开发语言·c++·qt·程序人生·用户界面
一 乐1 小时前
在线考试|基于Springboot的在线考试管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·毕设·在线考试管理系统
Byte Wizard1 小时前
动态内存管理
c语言·开发语言