mysql在repeatable read下幻读分场景:快照读(普通select)依赖mvcc天然免疫;当前读(select for update等)靠next-key lock防护,但仅当查询走索引时生效——无索引则间隙锁失效,幻读必然发生。

MySQL在REPEATABLE READ隔离级别下,幻读不是“没解决”,而是分场景:快照读天然免疫,当前读靠Next-Key Lock堵住插入缺口——但这个锁是否生效,完全取决于查询是否走索引。
为什么普通SELECT不会出现幻读,但SELECT FOR UPDATE会
根本区别在于读类型:普通SELECT是快照读(snapshot read),依赖MVCC生成的Read View,只看到事务启动时已提交的数据版本,新插入行即使提交了也进不来这个快照;而SELECT ... FOR UPDATE是当前读(current read),必须查最新数据,同时触发加锁逻辑——这时InnoDB才会尝试用Next-Key Lock封锁查询范围。
常见错误现象:SELECT * FROM t WHERE name LIKE 'a%'没走索引,第二次SELECT ... FOR UPDATE仍看到新插入行——因为没索引,间隙锁失效,锁不住空隙。
- 快照读不加任何锁,纯靠版本链过滤,幻读自然不存在
- 当前读才真正面对并发插入,Next-Key Lock此时是唯一防线
- 如果查询条件无法命中索引(如对无索引列做范围查询、LIKE前缀通配、函数包裹字段),InnoDB退化为表级扫描,间隙锁不启用,幻读必然发生
Next-Key Lock到底锁了什么:记录 + 间隙,缺一不可
Next-Key Lock = Record Lock(锁住匹配到的具体索引记录) + Gap Lock(锁住这些记录之间的间隙)。例如WHERE id > 100,假设现有索引值为[90, 110, 150],那么锁住的是(100, 110)和(110, 150)两个间隙,以及110、150这两条记录本身——这样事务B就无法插入id=105或id=130的新行。
容易踩的坑:INSERT语句自身不申请间隙锁,它只检查插入位置前后是否有Gap Lock或Next-Key Lock阻挡。若没有,插入直接成功,幻读就坐实了。
- 唯一索引上的等值查询(
WHERE id = 123)只加Record Lock,不加Gap Lock,无法防插入 - 非唯一索引的范围查询(
WHERE status = 1 AND created_at > '2025-01-01')才会激活Next-Key Lock,前提是status和created_at联合走索引 - 主键或唯一索引的范围查询(
WHERE id BETWEEN 100 AND 200)能完整覆盖间隙,防护效果最好
如何验证你的查询是否真的用了Next-Key Lock
不能只看执行计划(EXPLAIN),得查InnoDB的锁视图。在复现幻读场景后,连接数据库执行:
SELECT * FROM information_schema.INNODB_TRX;
再查当前被锁的资源:
SELECT * FROM information_schema.INNODB_LOCKS;
最后看锁等待关系:
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
关键确认点:LOCK_MODE字段显示X,GAP或X,NK(NK即Next-Key),且LOCK_DATA显示的不是单个值而是区间(如100, 110)——这才是临键锁生效的铁证。
- 如果
LOCK_MODE只有X,REC_NOT_GAP,说明只锁了记录,没锁间隙,幻读风险仍在 - 如果查询返回空结果集(如
WHERE id > 999999),InnoDB仍会对索引末尾间隙加Gap Lock,这点常被忽略 - 使用
SHOW ENGINE INNODB STATUS\G也能看到详细锁信息,但输出杂乱,优先用information_schema三张表
比加锁更可靠的替代方案:用唯一约束代替“先查后插”
很多幻读实际源于业务逻辑漏洞:比如“查用户是否存在→不存在则注册”,两个并发请求同时查不到,都去插,导致重复。这种场景下,Next-Key Lock反而难施力,因为第一次查询是快照读,不加锁。
正确做法是放弃应用层判断,直接依赖数据库约束:
INSERT INTO users (email, name) VALUES ('a@b.com', 'Alice') ON DUPLICATE KEY UPDATE name = VALUES(name);
前提是email字段有唯一索引。这样无论多少并发,最多一条插入成功,其余自动转为更新或忽略,从源头消灭幻读温床。
- 唯一约束强制串行化插入路径,比锁更轻量、更确定
- 避免长事务持有Next-Key Lock阻塞其他写入,提升吞吐
- 当业务确实需要“查+改”原子性时(如库存扣减),必须用
SELECT ... FOR UPDATE且确保WHERE走索引,否则宁可拆成存储过程加应用层分布式锁
真正容易被忽略的点:Next-Key Lock的生效前提是**查询必须落在索引有序结构上**。一旦出现隐式类型转换(如字符串字段存数字却用整数查询)、函数运算(WHERE YEAR(created_at) = 2025)或OR条件破坏索引连续性,间隙就漏了——幻读不是锁没加,是锁根本没机会加。











