MySQL在RR级别下,MVCC仅解决快照读的幻读,Next-Key Lock才真正防止当前读的幻读;二者机制独立,混用快照读与当前读是幻读主因。

不能。MVCC只对快照读(普通SELECT)提供一致性视图,不加锁、不阻塞插入,因此无法防止幻读;真正防幻读依赖的是当前读触发的Next-Key Lock,而它和MVCC是两套独立机制。
快照读不等于防幻读,只是固定了ReadView时间点
RR级别下,事务第一次执行SELECT时生成ReadView,后续所有无锁SELECT都复用它。新插入行只要其DB_TRX_ID大于ReadView的up_limit_id,就不可见——所以两次SELECT * FROM t WHERE b = 1结果一致。
但这只是“看起来没幻读”,不是隔离级别在保证范围稳定性:
- 快照读不加任何锁(包括间隙锁),既不阻止别人插入,也不感知已提交的新行
- 如果你用这个结果做存在性判断(比如“查不到就插入”),后续
INSERT或UPDATE可能因当前读看到新行而破坏逻辑 - 没有索引的
WHERE条件会导致全表扫描,快照读性能差,但幻读反而“不明显”——因为新行根本没被MVCC版本链纳入
当前读才触发Next-Key Lock,但只在特定条件下生效
SELECT ... FOR UPDATE、UPDATE、DELETE属于当前读,InnoDB必须访问最新已提交数据,并尝试加Next-Key Lock(记录锁 + 间隙锁)。但它有硬约束:
-
WHERE字段必须有有效索引:若name = 'Alice'的name列无索引,InnoDB退化为全表扫描,间隙锁失效 - 必须是范围条件:
WHERE id > 100会锁(100, +∞);而WHERE id = 100(主键等值)只加记录锁,不锁间隙,别人仍可在99和101之间插入 - 非唯一二级索引上等值查询(如
INDEX(b)且b = 10有多行),InnoDB向右扫描直到第一个不满足条件项,容易漏锁或过度锁
混用快照读和当前读是幻读高发场景
这是生产中最常踩的坑:事务内先用SELECT拿到旧视图,再用UPDATE或SELECT ... FOR UPDATE穿透MVCC——两次读语义不同,结果集天然可能不一致。
典型流程:
MySQL 数据库管理技能,能够通过自然语言查询和管理数据库,支持 SELECT/INSERT/UPDATE/DELETE、表管理、备份恢复等操作。当用户提及 MySQL、数据库查询、建表或数据备份时使用此技能。
- 事务A执行
SELECT COUNT(*) FROM orders WHERE user_id = 123得0 - 事务B插入
user_id = 123并提交 - 事务A执行
INSERT INTO orders (...) VALUES (...)(无唯一约束)成功 - 事务A再
SELECT发现多出1行
问题不在RR失效,而在第一次SELECT基于旧ReadView,第二次INSERT或UPDATE是当前读,直接看到并锁定最新数据。
显式加锁才是可控手段,但代价明确
依赖RR默认行为防幻读风险极高。真正可控的方式是主动控制读取模式:
- 所有需要强一致性的范围查询,统一改用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE - 避免在同一个事务中混用普通
SELECT和FOR UPDATE——要么全快照读(接受幻读可能性),要么全当前读(用锁兜底) - 如果业务允许,把事务起始点设为
BEGIN后立即执行一次当前读,提前建立锁范围
注意:Next-Key Lock只保护加锁之后的插入,无法追溯拦截“加锁之前”已存在的快照盲区;且锁粒度扩大,在高并发插入场景下容易引发死锁或锁等待超时。
最易被忽略的一点:幻读是否发生,不取决于你用了什么隔离级别,而取决于你用了什么读模式、有没有索引、WHERE条件是否能走索引、以及是否在同一事务里切换读类型——这些细节比隔离级别本身更关键。










