MySQL RR隔离级别需配合当前读、索引条件及范围查询才能触发Next-Key Lock防幻读;无索引或等值查询仅加记录锁,无法锁间隙,故status='pending'防不住而id>10可防住。

MySQL 的 RR 隔离级别本身不“自动防止幻读”,必须配合当前读 + 索引条件 + 范围查询,才能触发 Next-Key Lock 生效;否则幻读照常发生。
为什么 SELECT * FROM t WHERE id > 10 可能防住幻读,而 SELECT * FROM t WHERE status = 'pending' 防不住
Next-Key Lock 是 Record Lock(锁记录)和 Gap Lock(锁间隙)的组合,但它只在满足两个硬性前提时才启用:查询走索引、且是范围条件。前者没索引,InnoDB 只能全表扫描,退化为表级锁或干脆不锁间隙;后者是等值查询且命中唯一索引(如主键),InnoDB 只加 Record Lock,不锁前后间隙——别人照样能在 id=9 和 id=11 之间 INSERT 新行。
-
id > 10:若id是主键或二级索引,会锁住 (10, +∞) 这个区间,新插入 id=15 的行会被阻塞 -
status = 'pending':若status字段无索引,InnoDB 不知道“pending”在哪儿,无法界定间隙,只能锁表或不锁 - 即使
status有索引,但用的是LOCK IN SHARE MODE(S 锁),InnoDB 默认不加 Gap Lock,只锁命中的行
怎么确认你的查询真的用了 Next-Key Lock
不能光看隔离级别设成了 REPEATABLE READ,得查锁的实际行为。最直接的方式是查 performance_schema.data_locks 表,过滤出当前事务的锁记录:
SELECT LOCK_TRX_ID, LOCK_MODE, LOCK_DATA FROM performance_schema.data_locks WHERE LOCK_TRX_ID = '你的事务ID';
关键看 LOCK_MODE 字段:出现 RECORD & GAP 或 RECORD & NEXT-KEY 才算生效;如果只有 RECORD,说明只是行锁;如果压根没记录,可能连锁都没加。
- 另一个验证方式:在事务 A 中执行
SELECT * FROM t WHERE id > 5 FOR UPDATE,然后在事务 B 中尝试INSERT INTO t VALUES (6, ...)—— 若被阻塞,说明间隙被锁住 - 注意:
SHOW ENGINE INNODB STATUS\G中的lock_mode X locks rec but not gap明确表示没用上间隙部分
哪些配置或写法会让 Next-Key Lock “悄悄失效”
Next-Key Lock 很依赖运行时上下文,稍有偏差就降级。常见失效点包括:
- MySQL 8.0+ 中残留了
innodb_locks_unsafe_for_binlog = ON(已废弃但旧配置可能还在),它会强制禁用 Gap Lock - 使用了
SELECT ... LOCK IN SHARE MODE+ 唯一索引等值查询 → 只加 S 类 Record Lock,不锁间隙 - 查询条件含函数或类型隐式转换,比如
WHERE DATE(create_time) = '2026-04-17',导致索引失效,进而无法启用 Next-Key - 事务中先执行了快照读(普通
SELECT),再执行当前读,MVCC 视图已固定,但锁是后加的,间隙保护只对后续插入生效,对之前已发生的插入无回溯能力
开发时没复现幻读,上线后突然冒出,为什么
本地测试数据少,索引页内记录密集,间隙小甚至不存在;线上数据量大、B+ 树层级深,同一索引页内相邻记录间隔变大,间隙变宽,其他事务更容易在空档中成功 INSERT —— 这时候你才发现,原来自己一直没真正锁住那个“空”。更麻烦的是,这种问题往往只在高并发写入场景下暴露,单线程压测根本撞不出来。
所以别依赖“没看到幻读=已解决”,得盯住锁的实际行为,而不是业务日志里有没有多出一行。


















