MySQL 可重复读隔离级别下是否完全解决了幻读

在 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 UPDATESELECT ... 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 可重复读隔离级别下是否完全解决了幻读

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏