RC隔离级别下幻读必然存在,因其仅使用记录锁而不加间隙锁,无法阻止其他事务在索引间隙中插入新行,且每次SELECT都重建Read View,导致同一事务内多次查询可见新插入的已提交行。

RC隔离级别下幻读为何必然存在
MySQL在READ COMMITTED(RC)隔离级别下**不解决幻读**,这是设计使然,不是bug。RC只保证“已提交的数据可见”,但完全不阻止其他事务插入新行——而幻读的本质,就是新插入的行在当前读中突然出现。
关键在于:RC禁用间隙锁(Gap Lock),只使用记录锁(Record Lock)。这意味着它能锁住已存在的匹配行,但对索引区间之间的“空隙”毫无防护。只要WHERE条件没命中唯一索引,或查询走全表扫描,插入就畅通无阻。
-
SELECT * FROM user WHERE age = 23—— 若age无索引,RC下该查询不加任何间隙锁,事务B随时可INSERT INTO user (name, age) VALUES ('王五', 23) - 即使
age有普通索引,RC也只锁住值为23的现有记录,不封锁(22, 24)这类间隙,插入age=23仍可能成功(尤其当存在重复值时) - RC的MVCC Read View按语句级重建,每次
SELECT都看到最新已提交版本,所以第二次查询自然能看见事务B刚插入并提交的行
RC与RR在幻读处理上的核心差异
很多人误以为“RR解决了幻读”,其实准确说是:**RR在特定条件下能抑制幻读,而RC完全放弃抑制**。这个差异全系于锁策略:
- RR默认启用间隙锁(Gap Lock)和临键锁(Next-Key Lock),前提是查询走索引;例如
SELECT * FROM t WHERE id > 100 FOR UPDATE会锁住(100, +∞)区间,阻止插入id=105 - RC下
SELECT ... FOR UPDATE也只加记录锁,不加间隙锁——哪怕条件字段有索引,它也不会封锁间隙,插入照样发生 - RC没有“事务启动时固定Read View”的机制,每次快照读都重新生成Read View,导致同一事务内两次查询可能看到不同提交状态的插入行
哪些操作在RC下一定会触发幻读
不是所有RC查询都会暴露幻读,但以下场景几乎必现,需特别警惕:
- 范围查询 + 无索引字段:
SELECT COUNT(*) FROM orders WHERE status = 'pending' AND created_at > '2026-09-01',若status或created_at未联合索引,RC下完全不锁间隙 - 等值查询 + 非唯一索引 + 存在重复值:
SELECT * FROM log WHERE level = 'ERROR' FOR UPDATE,若level是普通索引且多行值为'ERROR',RC只锁这些行,不锁它们之间的间隙 - 先查后插逻辑:
SELECT id FROM users WHERE email = 'a@b.com'; -- 返回空 → INSERT INTO users (email) VALUES ('a@b.com');,RC下两次操作之间,其他事务可抢先插入同email,导致唯一约束冲突
RC下想避免幻读,只能靠应用层补救
数据库层已放弃,就得在代码里兜底。这不是权宜之计,而是RC语义下的合理选择:
- 改用
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO,把“查-判-插”压缩为原子语句,绕过幻读窗口 - 对关键业务路径,在应用层加分布式锁(如Redis锁),确保同一
email或order_no的校验与插入串行执行 - 若必须依赖范围查询结果做后续更新,改用
SELECT ... FOR UPDATE并确认执行计划走了索引——但要知道,这在RC下仍不防插入,仅防更新冲突
真正容易被忽略的是:RC下SELECT ... FOR UPDATE看似加了锁,实则锁力极弱;你以为封住了数据,其实只封住了已有行,缝隙大开着。要防幻读,要么升RR+建对索引,要么别指望锁,老实用唯一约束+原子写入兜底。


















