mysql可重复读隔离级别不能完全解决幻读,仅通过mvcc和next-key lock在多数场景下抑制幻读;快照读后当前读、无索引范围查询等例外路径仍会导致结果集行数变化的幻读现象。

MySQL 的可重复读(REPEATABLE READ)隔离级别不能完全解决幻读,它只是在大多数常见场景下通过 MVCC + Next-Key Lock 机制大幅抑制了幻读,但存在明确的例外路径——只要触发“快照读后当前读”或“无索引范围查询”,幻读就可能真实发生。
幻读的本质是结果集行数变化,不是单行内容变更
幻读(Phantom Read)指同一事务中,两次执行相同条件的 SELECT,第二次返回了第一次没有的“新行”(或少了原有行),导致结果集行数不一致。例如:
- 事务 A 执行
SELECT * FROM orders WHERE status = 'pending',查到 2 行; - 事务 B 插入一条
status = 'pending'的新订单并提交; - 事务 A 再次执行相同语句,查到 3 行 —— 这就是幻读。
注意:这和不可重复读有本质区别——后者是同一行的字段值被改了(如金额从 100 变成 200),而幻读关注的是“有没有多出/少掉一行”。
可重复读靠 MVCC 拦住快照读,但拦不住当前读
REPEATABLE READ 下,InnoDB 对两类读行为分别处理:
-
SELECT(无锁)是快照读:事务第一次执行时生成Read View,后续所有普通SELECT都复用它,因此永远看不到其他事务插入的新行 —— 这部分幻读被 MVCC 拦住了; -
SELECT ... FOR UPDATE、UPDATE、DELETE是当前读:不走Read View,直接读最新已提交版本,并加锁; - 问题就出在这里:如果事务 A 先做一次快照读(没看到新行),再做一次
SELECT ... FOR UPDATE,它会突然“发现”那条新行 —— 因为当前读绕过了快照,直接看到事务 B 提交的结果。
典型错误模式:SELECT id FROM user WHERE name = 'Alice' 没查到 → 紧接着 INSERT INTO user (name) VALUES ('Alice') 报主键/唯一键冲突 —— 这就是由快照读与当前读语义不一致引发的幻读现象。
Next-Key Lock 不是万能的,索引缺失或唯一等值查询会失效
Next-Key Lock(记录锁 + 间隙锁)本该阻止其他事务在查询范围内插入,但它依赖两个前提:
- 查询必须走索引(最好是范围条件,如
WHERE age > 20); - 如果是唯一索引的等值查询(如
WHERE id = 100),InnoDB 通常只加行锁,不加间隙锁 —— 此时其他事务仍可在id = 100附近插入新行(比如id = 99或id = 101),造成幻读; - 若查询条件无法使用索引(例如
WHERE status = 'pending',而status列没建索引),InnoDB 退化为全表扫描 + 行锁,间隙锁失效,幻读风险回归。
换句话说,SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 这条命令本身不保证防幻读,真正起作用的是你写的 SQL 是否命中索引、是否显式加锁、以及是否混用快照读和当前读。
真正容易被忽略的点:长事务 + 多次读取 + 无显式锁
很多线上问题不是发生在隔离级别设置错误,而是出现在业务逻辑里:一个长达几分钟的事务,中间穿插多次普通 SELECT 和一次 UPDATE,且没加 FOR UPDATE。此时,MVCC 快照锁定的是事务启动时刻,但 UPDATE 是当前读,会看到中间其他事务插入的数据 —— 这种“读-写不一致”比纯查询幻读更难排查,也更危险。不要以为设了 REPEATABLE READ 就万事大吉,关键操作前必须确认是否需要加锁、是否走索引、是否应缩短事务生命周期。











