这道题我见过太多人翻车
"MySQL 可重复读隔离级别下,会出现幻读吗?"
这题几乎是后端面试的必考项。大部分人张口就是"不会,因为 MVCC"。面试官追一句"那 MVCC 具体怎么防的",很多人就开始卡壳,或者干脆把 MVCC 和加锁混成一锅粥。
我自己第一次被问到这题是校招面某家电商公司,当时答得就是"不会,MVCC",然后被追问"那如果我用 SELECT ... FOR UPDATE 呢",直接懵了。后来花了挺长时间才把这条链路理清楚:MVCC 和加锁管的其实是两码事,一个管快照读,一个管当前读,答案不是简单的"会"或"不会",得分情况说。这篇把这条链路完整过一遍。
先分清两种读
InnoDB 里读分两种:
快照读 :普通的 SELECT,不加任何锁,读的是某个时间点的历史版本。
当前读 :SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE,以及 UPDATE、DELETE、INSERT 这些写操作,读的是最新版本,并且会加锁。
这两种读防幻读的机制完全不是一回事------前者靠 MVCC,后者靠 Next-Key Lock。混着讲,是大部分人卡壳的根源。
快照读怎么防幻读:MVCC
可重复读级别下,一个事务开启后第一次执行快照读时,会生成一个 ReadView,记录当前系统里"活跃"的事务 ID 列表。之后这个事务里所有的快照读,都复用同一个 ReadView------这就是"可重复读"名字的由来。
每一行数据在 InnoDB 里都有隐藏的 trx_id(最后修改它的事务 ID),还有一条通过 undo log 串起来的版本链,可以往回倒推出这行数据历史上每个版本长什么样。
判断某个版本对当前事务是否可见,大致是拿这行的 trx_id 去跟 ReadView 里记的那几个边界值和活跃事务列表比对:
- 如果这个版本是当前事务自己改的,可见。
- 如果
trx_id小于 ReadView 里最小的活跃事务 ID,说明这个版本在生成 ReadView 之前就已经提交了,可见。 - 如果
trx_id在活跃事务列表里,说明改这行的事务在生成 ReadView 时还没提交,不可见,顺着 undo log 版本链往前找上一个版本继续判断。 - 如果
trx_id大于等于 ReadView 生成时的下一个事务 ID,说明这是"未来"才发生的修改,不可见。
因为 ReadView 全程复用,别的事务后来插入的新行,其 trx_id 必然比这个事务能看见的范围要新,所以在这个事务眼里,这行数据从来没出现过。同一个事务里查一百次,结果都一样------快照读层面,幻读被 MVCC 挡住了。
这也是为什么面试官问"可重复读怎么防幻读",标准答案第一步永远是先讲 MVCC。但只讲到这一步是不完整的,漏掉了当前读那部分。
当前读为什么还会有问题
MVCC 只对快照读生效。一旦事务里用了 FOR UPDATE、或者执行了 UPDATE/DELETE,读的就是最新数据,MVCC 直接失效。
这时候如果没有额外的锁机制,经典的幻读场景就会发生:
事务 A 执行 SELECT * FROM orders WHERE id BETWEEN 10 AND 20 FOR UPDATE,拿到 id=10 和 id=20 两行。事务 B 这时候插入一条 id=15 的记录并提交。事务 A 如果紧接着再查一次同样的条件,会发现多出来一行------这就是当前读语义下的幻读。
InnoDB 用 Next-Key Lock 堵住这个口子。
Next-Key Lock:记录锁 + 间隙锁
Next-Key Lock 不是单一的锁,是"记录锁(Record Lock)"加"间隙锁(Gap Lock)"的组合。记录锁锁住已存在的行本身,间隙锁锁住两条记录之间那段"空隙"。注意,间隙锁锁的是一个区间而不是某一行,所以别的事务在这个区间里插入新记录,会直接被挡住。

还是上面那个例子:表里已有 id=5、10、20、25 四条记录。事务 A 执行 WHERE id BETWEEN 10 AND 20 FOR UPDATE,InnoDB 会把 (10, 20] 这个区间连同两端记录一起锁住,也就是间隙 (5,10] 之后到记录 20 为止的这一整段。这时候事务 B 想插入 id=15,会命中这段间隙锁,直接被阻塞,一直等到事务 A 提交或回滚。
再往下追一句会更有意思:如果查询条件是唯一索引上的等值查询(比如 WHERE id=10 FOR UPDATE),命中了一条已存在的记录,Next-Key Lock 会退化成单纯的记录锁,不会锁间隙------因为唯一索引保证了这个值不可能再插入第二条,锁间隙没有意义。这个退化条件是面试官很爱拿来追问的细节。
另外插入操作本身还有个"插入意向锁(Insert Intention Lock)",本质是一种特殊的间隙锁,专门用来降低并发插入时的锁冲突------只要两个插入操作实际插入的位置不冲突,它们对同一个间隙的插入意向锁互相不阻塞。这个知道就行,面试很少往这么细问。
完整对答一遍
把上面这套逻辑串起来,面试时大概可以这样答。
先说结论:要分快照读和当前读两种情况。
快照读层面,可重复读级别下同一个事务全程复用同一个 ReadView,基于 trx_id 和 undo log 版本链做可见性判断,别的事务新插入的行对当前事务永远不可见,所以快照读不会有幻读。
当前读层面,MVCC 不生效,InnoDB 靠 Next-Key Lock(记录锁加间隙锁)把查询涉及的区间锁住,阻止其他事务在这个区间插入新记录,所以当前读也不会有幻读。但这依赖 InnoDB 的具体实现,SQL 标准定义的可重复读级别本身并不保证防幻读,MySQL 是在标准之上做了加强。
追问环节常见的坑:唯一索引等值查询命中已有记录时锁会退化成记录锁;READ COMMITTED 级别下没有间隙锁,所以那个级别下当前读也会有幻读。这两点答上,基本能把这题聊透了。
顺手说一句
这类连环追问的题,光背答案没用,得真正把链路理顺才经得起连续问下去。我自己后来准备面试的时候,除了啃官方文档,也会用面灵这类工具做几轮模拟追问练习,专门打磨哪个环节容易被问倒------工具是辅助,但这道题本身该弄懂的原理一点都不能少。
如果这道题你之前也是含糊带过,建议自己动手开两个事务窗口,用 EXPLAIN 实测一遍 Next-Key Lock 的加锁范围,比看多少篇文章都管用。