MVCC(多版本并发控制)并不能完全解决幻读问题,其能力边界主要取决于具体的操作类型(快照读 vs. 当前读)以及数据库的实现机制。
下表清晰地对比了MVCC在不同场景下对幻读问题的处理方式。

💡 理解"快照读"与"当前读"
要理解上表的结论,首先要明白这两种读取方式的区别:
• 快照读:这是不加锁的普通 SELECT 操作。在可重复读(RR)隔离级别下,事务在第一次执行 SELECT 时会创建一个 Read View(读视图),相当于给数据库拍了一张快照。此后在该事务内的所有快照读都会基于这个相同的 Read View 来读取数据,因此看不到其他事务在此之后提交的修改,从而避免了幻读 。
• 当前读:这类操作需要获取锁以保证数据逻辑的一致性。例如,UPDATE 语句在修改前必须知道数据的最新状态,否则可能覆盖其他事务的更新。因此,当前读会绕过 MVCC 的快照机制,直接读取最新的数据版本,如果其他事务插入了符合条件的新行,就会被当前读看到,导致幻读 。
🔍 一个典型的幻读场景
假设有一个简单的用户表,初始只有一条记录 id=1。

场景分析:
• 在 T4 时刻,事务A的第二次快照读结果和 T2 时刻一致,此时MVCC成功避免了幻读。
• 问题出在 T5 时刻。当事务A执行 UPDATE(当前读)时,它读到了事务B已提交的新数据 id=2,并成功更新了这行。
• 在 T6 时刻,当事务A再次执行快照读时,由于事务A自己更新了 id=2 这行记录,这个修改对事务A是可见的,因此查询结果变成了两条,出现了幻读 。这个例子说明,即使在RR级别下,事务内部的当前读操作也可能为幻读打开缺口。
🛠️ 如何彻底解决幻读
既然MVCC能力有限,要彻底解决幻读问题,需要组合拳:
-
使用 Next-Key Lock(临键锁)
这是InnoDB在可重复读(RR)隔离级别下防止当前读出现幻读的核心机制。Next-Key Lock是 行锁(Record Lock) 和 间隙锁(Gap Lock) 的结合。它不仅锁住记录本身,还会锁住记录之间的间隙,防止其他事务在查询范围内插入新数据 。
◦ 例如:执行 SELECT * FROM users WHERE id > 0 FOR UPDATE; 时,InnoDB会锁住 id > 0 的整个范围。此时如果事务B尝试插入 id=2 的记录,会被阻塞,直到事务A提交,从而避免了幻读。
-
提升隔离级别至串行化(Serializable)
这是最严格的方案。在该级别下,读写操作会默认加锁,事务强制串行执行,从根本上杜绝了并发问题,包括幻读。但这也是以牺牲并发性能为代价的 。
💎 总结
总的来说,**MVCC是一项优秀的并发控制技术,它通过快照读极大地缓解了数据库的锁竞争,提升了性能,并在快照读场景下避免了幻读。**但它并非幻读的"银弹"。
• 核心局限:MVCC的"免疫"效果仅限于快照读。一旦事务内混用了当前读(如UPDATE),就必须依赖 Next-Key Lock 这种锁机制来保证数据的绝对一致性 。
• 实践建议:在大多数应用场景下,MySQL的RR隔离级别(MVCC + Next-Key Lock)已经能很好地平衡一致性和性能。开发者在编写事务时,需要留意当前读可能带来的影响,对于关键业务逻辑,可以考虑在事务开始时就用 SELECT ... FOR UPDATE 对关键范围进行锁定。