MVCC本身不解决幻读,仅保证快照读下两次查询行数一致;真正阻止幻读的是Next-Key Lock,它通过记录锁+间隙锁锁定值区间,阻塞符合条件的新插入。

MVCC本身不解决幻读,只解决快照读下的“看起来像幻读”的现象
很多人误以为 MVCC 直接“解决了幻读”,其实不是。MySQL 官方定义的幻读是:同一事务中,两次相同条件的 SELECT 返回行数不同。而 MVCC 仅保证你在 SELECT(无锁、快照读)时看到的是事务启动时刻的一致性视图——它让你“看不见”其他事务新插入的行,所以两次 SELECT 行数一样。但这只是“视觉上没幻读”,不是真正阻止了插入。
关键点在于:MVCC 对 INSERT、UPDATE、DELETE 或带锁的 SELECT ... FOR UPDATE 完全不设防。这些操作走的是“当前读”,绕过 MVCC 快照,直接访问最新版本并加锁。
- MVCC 只作用于普通
SELECT(即快照读),不干预写入路径 - 只要另一个事务在你事务执行期间
INSERT并COMMIT了一条满足你查询条件的新记录,你的下一次“当前读”就会立刻看到它 - 如果你用的是
READ COMMITTED隔离级别,每次快照读都生成新ReadView,连“视觉上”的一致性都保不住——更易出现行数变化
真正拦住幻读的是 Next-Key Lock,不是 MVCC
在 REPEATABLE READ 级别下,InnoDB 对范围查询(如 WHERE id > 10)自动使用 Next-Key Lock:它 = Record Lock(锁住命中的索引记录) + Gap Lock(锁住记录之间的间隙)。这个组合锁住的是“值区间”,不是单点。
比如你执行 SELECT * FROM t WHERE age > 25 FOR UPDATE,InnoDB 不只锁住现有 age > 25 的行,还会锁住所有可能插入 age > 25 新记录的间隙(例如 (25, +∞))。此时另一个事务尝试 INSERT INTO t VALUES (..., 28) 就会被阻塞,直到你事务结束。
- 没有索引?那会退化为表级锁(或全表扫描+大量间隙锁),性能极差
-
SELECT ... LOCK IN SHARE MODE同样触发 Next-Key Lock,不只是FOR UPDATE - 唯一索引等值查询(
WHERE id = 100)只用 Record Lock,不加 Gap Lock —— 此时仍可能发生幻读(比如插入id = 100但其他字段不同的重复行,若无唯一约束)
快照读 vs 当前读混用,才是幻读“复现”的典型场景
最常让人困惑的幻读案例,往往源于在一个事务里交替使用两种读法:
事务 A 先执行 SELECT * FROM t WHERE score > 80(快照读,看到 3 行)→ 事务 B 插入 score = 85 的新行并提交 → 事务 A 再执行 UPDATE t SET bonus = 100 WHERE score > 80(这是当前读!会扫描最新数据,并更新包括 B 插入的那行)→ 事务 A 最后 SELECT * FROM t WHERE score > 80(又是快照读,但这次 ReadView 是事务开始时生成的,仍看不到 B 的行)→ 表面看“更新了 4 行但只查出 3 行”,就以为幻读发生了。
- 问题不在 MVCC 失效,而在你把“读一致性”和“写可见性”当成一回事
-
UPDATE和DELETE的 where 条件匹配逻辑永远基于最新数据(当前读),与快照读的 ReadView 无关 - 如果真要全程一致,就得统一用当前读(全部加锁),或者接受 MVCC 带来的“读写分离”语义
RR 级别下幻读是否“被解决”,取决于你怎么定义“解决”
MySQL 文档说 RR “防止幻读”,指的是:在纯快照读场景下不会出现行数变化;在当前读场景下,通过 Next-Key Lock 阻止符合条件的新记录插入。但它无法防止“快照读 + 当前读混合使用”导致的认知冲突。
一个容易被忽略的细节是:Gap Lock 在唯一索引的等值查询中不生效,且 INSERT ... ON DUPLICATE KEY UPDATE 这类语句也可能绕过间隙保护。这意味着即使在 RR 下,特定写法仍可能引入幻影行。
- 业务逻辑若依赖“两次 SELECT 行数不变”,必须确保两次都是快照读,且中间不穿插任何当前读操作
- 想彻底杜绝幻影插入?得靠应用层唯一约束 + 数据库唯一索引,而不是只靠隔离级别
- MVCC 和 Next-Key Lock 是协作关系,不是替代关系:一个管“读的视角”,一个管“写的边界”


















