在 MySQL 的面试中,有一个问题几乎成了区分候选人水平的分水岭:“可重复读隔离级别下,幻读被完全解决了吗?”
很多人的第一反应是“解决了”,因为教科书上白纸黑字写着:可重复读(RR)隔离级别通过 MVCC 和间隙锁解决了幻读。但如果你真的这么回答,面试官往往会追问一句:“你确定吗?有没有例外情况?”这时候,真正的考验才刚刚开始。
什么是幻读
先把概念理清楚。幻读是指在同一个事务中,两次执行相同的范围查询,第二次查询却看到了第一次查询没有看到的行。注意,这里强调的是“行数的变化”,而不是“某一行的数据变化”。后者属于不可重复读的范畴。
举个例子:事务 A 先执行 SELECT * FROM users WHERE age > 20,返回 5 行。接着事务 B 插入了一条 age = 25 的新记录并提交。事务 A 再次执行同样的查询,如果返回了 6 行,那就是幻读。
MySQL 在 RR 级别下如何“解决”幻读
MySQL 的 InnoDB 引擎在可重复读级别下,主要通过两种机制来应对幻读:
第一,MVCC(多版本并发控制)。 对于快照读(普通的 SELECT 语句),InnoDB 利用 ReadView 来决定当前事务能看到哪个版本的数据。事务启动时生成的 ReadView 会一直沿用到事务结束,因此即使其他事务插入了新行并提交,当前事务的快照读也看不到这些新行。这确实在快照读场景下避免了幻读。
第二,间隙锁(Gap Lock)+ Next-Key Lock。 对于当前读(SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE),InnoDB 不仅锁住符合条件的行,还会锁住行与行之间的“间隙”,防止其他事务在间隙中插入新记录。Next-Key Lock 是记录锁和间隙锁的组合,锁住的是一个左开右闭区间。
正是这两套机制配合,让 MySQL 在 RR 级别下很大程度上避免了幻读。但“很大程度上”和“完全”之间,差着一段距离。
幻读并未被完全解决的场景
场景一:快照读与当前读混用
这是最经典的“翻车”场景。假设有一张表 t,其中 id 是主键,初始只有一行数据 id = 1。
事务 A 开始,执行快照读:
SELECT * FROM t; -- 返回 1 行
此时事务 B 插入 id = 2 并提交。
事务 A 继续执行当前读:
SELECT * FROM t FOR UPDATE; -- 返回 2 行!
事务 A 明明在同一个事务里,第一次只看到 1 行,现在却看到了 2 行——幻读出现了。原因在于,快照读走的是 MVCC 的 ReadView,而当前读走的是最新版本的数据加锁读取。两者用的是不同的读取机制,自然可能看到不同的结果。
场景二:先快照读,再更新,再快照读
还是上面的表结构。事务 A 先快照读,看到 1 行。事务 B 插入 id = 2 并提交。事务 A 执行:
UPDATE t SET name = 'x' WHERE id = 2;
这条 UPDATE 是当前读,它能看到事务 B 插入的那行并成功更新。然后事务 A 再次快照读:
SELECT * FROM t; -- 返回 2 行!
为什么?因为事务 A 自己更新了 id = 2 这行,根据 MVCC 规则,事务能看到自己修改过的记录。所以原本在 ReadView 中不可见的新行,因为被本事务更新了,突然变得可见了。这同样构成了幻读。
场景三:没有索引的范围查询
间隙锁的效果依赖于索引。如果查询条件没有走索引,InnoDB 可能会锁住整张表的所有间隙,这种情况下反而不会出现幻读——但代价是并发性能急剧下降。反过来,在某些索引边界条件下,间隙锁的加锁范围可能不如预期,仍然存在幻读的风险。
为什么会出现这些“例外”
根本原因在于:MVCC 和间隙锁各自只覆盖了一部分场景,它们的组合并不能覆盖所有读写组合。
- MVCC 解决的是快照读之间的可重复性。
- 间隙锁解决的是当前读之间以及当前读与插入之间的冲突。
- 但当事务中同时存在快照读和当前读,且两者交替执行时,两套机制之间的“缝隙”就暴露出来了。
换句话说,MySQL 的 RR 级别保证了“一致性读”的语义,但并没有保证“整个事务生命周期内所有读操作看到的数据完全一致”。它保证的是快照读看到的是事务开始时的快照,当前读看到的是最新数据,而这两者本来就可以不同。
面试中该怎么回答
如果面试官问你“RR 是否完全解决了幻读”,一个加分的回答应该是:
在 MySQL InnoDB 的可重复读隔离级别下,幻读并没有被完全解决。对于单纯的快照读,MVCC 可以避免幻读;对于单纯的当前读,间隙锁和 Next-Key Lock 可以避免幻读。但当事务中快照读和当前读混用,或者先快照读再执行当前读的更新操作时,仍然可能出现幻读。严格来说,RR 级别下幻读是被“很大程度地抑制”了,而不是被“完全消灭”了。如果业务对幻读零容忍,应该考虑使用串行化隔离级别,或者在应用层加锁来保证。
这样的回答既展示了你对底层机制的理解,也体现了你对边界条件的敏感度——这正是高级工程师和普通工程师之间的差距。
总结
MySQL 的 RR 隔离级别是一个精妙的设计,它在性能和一致性之间取得了很好的平衡。但“平衡”意味着妥协,而幻读就是这种妥协留下的一个缺口。理解这个缺口在哪里、什么时候会暴露出来,比简单记住“RR 解决了幻读”这个结论要重要得多。毕竟,面试官想考察的不是你的记忆力,而是你对系统行为的真实理解程度。
未经允许不得转载:任鹏个人博客 » MySQL 可重复读隔离级别下是否完全解决了幻读

