mysql在read committed下幻读多,根本原因是innodb不加间隙锁(gap lock),仅对命中行加行锁,无法阻止其他事务在查询范围间隙中插入新行,导致同一事务内两次范围查询结果行数不一致。

MySQL 在 READ COMMITTED 隔离级别下幻读多,根本原因就一个:InnoDB 彻底不加 Gap Lock。 它靠 MVCC + 行锁应对并发,但对“间隙插入”完全不设防——只要别的事务在你查询范围里插了一行,你再查一次,就多出来一条“幻影”。这不是 bug,是设计使然。
READ COMMITTED 为什么禁用 Gap Lock
这个隔离级别下,InnoDB 明确放弃间隙保护:所有 SELECT ... FOR UPDATE、UPDATE、DELETE 只锁定**实际命中的记录**(record lock),不锁前后空隙。哪怕你写 WHERE id > 100,也只锁住现有 id > 100 的行,不会阻止别人插入 id = 105 的新行。
对比 REPEATABLE READ:后者默认用 next-key lock(record lock + gap lock),把“100 到下一个索引值之间”的空隙也锁死。
- READ COMMITTED 的目标是降低锁粒度、提升并发,代价就是接受幻读
- 它仍能防止脏读(靠 MVCC 版本判断)和不可重复读(更新时重读最新已提交版本)
- Gap Lock 被完全绕过,
EXPLAIN看执行计划也看不到“Using index condition”之外的额外锁范围提示
哪些场景下幻读会立刻暴露
不是所有查询都容易出幻读,关键看是否形成“可被插入的间隙”且被其他事务利用。典型触发点:
- 范围查询后紧接着做业务判断再插入,比如:
SELECT COUNT(*) FROM orders WHERE status = 'pending'返回 0,你决定创建新 pending 单,但并发事务抢先插入了一条,你再查就变成 1 - 非唯一索引上的等值查询:
SELECT * FROM user WHERE name = 'alice' FOR UPDATE—— 如果 name 不唯一,InnoDB 在 RC 下只锁命中行,不锁 name='alice' 前后可能插入的位置 - 没走索引的查询:
WHERE created_at > '2026-04-01'但该字段无索引 → 全表扫描 + 每行 record lock,间隙完全敞开
Gap Lock 缺失带来的连锁影响
表面只是“多查出一行”,但实际常引发逻辑错乱或数据覆盖:
- 应用层做“存在性校验 + 插入”时,RC 下校验和插入不是原子的,
INSERT IGNORE或ON DUPLICATE KEY UPDATE成了刚需 - 分页查询(
LIMIT offset, size)在 RC 下极不稳定:第一页查完,别人插了几条,第二页就跳过或重复 - 外键约束检查不触发 Gap Lock,子表插入可能绕过父表范围限制(例如父表只允许 status IN (1,2),但子表插入时父表刚好新增了 status=3 的行,RC 下不拦)
- 性能上看似更好(锁少),但应用不得不加更多补偿逻辑,整体复杂度反而上升
想规避幻读,又不想升到 SERIALIZABLE 怎么办
REPEATABLE READ 是折中选择,但它不是银弹——Gap Lock 一开,死锁概率陡增,尤其在非唯一索引或范围更新密集的场景。真正可控的做法是:
- 把关键判断逻辑收口到单条语句:用
INSERT ... SELECT或带WHERE NOT EXISTS的插入,让校验和写入在同一个锁上下文中完成 - 对必须强一致的范围操作,显式加锁并升事务级别:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ,但务必确认该事务内没有长耗时非数据库操作 - 避免在 RC 下依赖“两次查询结果一致”这个假设;如果业务本质需要,不如承认这是 RC 的能力边界,换方案而非硬扛
Gap Lock 的开关不在代码里,而在隔离级别和索引设计的交叉点上。很多人调优时只盯着 SQL 和索引,却忘了 transaction_isolation 这个全局开关才是间隙行为的总闸门。











