在 RR(可重复读)隔离级别下,当前读通常会触发间隙锁。这是 InnoDB 为了保证可重复读语义而强制采用的 Next-Key Lock 锁机制,它由记录锁与间隙锁共同组成,通过覆盖索引区间与索引间隙来防止幻读问题。

当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)在 RR 隔离级别下会触发间隙锁,这并非可选行为,而是 InnoDB 为实现 MySQL 可重复读与防止幻读而强制执行的锁策略。
RR 级别下当前读默认采用 Next-Key Lock
在 MySQL 5.7 及以上版本的 InnoDB 存储引擎中,当事务隔离级别为 REPEATABLE READ 时,当前读操作默认会应用 Next-Key Lock。这种锁机制本质上是记录锁(Record Lock)与间隙锁(Gap Lock)的组合,不是“可能会加”,而是 InnoDB 在该隔离级别下的默认并发控制方式。
- 即使只是查询一个不存在的主键:
SELECT * FROM t WHERE id = 1000 FOR UPDATE,而表中最大id为 999,InnoDB 仍会锁住(999, +∞)这一段索引间隙 - 对于范围查询,这种表现会更明显:
SELECT * FROM t WHERE age > 25 FOR UPDATE会锁定满足条件的索引范围,以及这些索引值之间可能被插入新记录的空隙 - 唯一索引上的
INSERT也会涉及间隙锁:插入之前需要先申请目标区间的锁,以避免并发插入影响唯一性约束
为什么普通 SELECT 不会触发,而当前读必须加锁?
快照读(普通 SELECT)依赖 MVCC 多版本并发控制,通过读取历史版本来避免幻读;而当前读需要读取最新数据并控制后续修改边界,因此必须锁住“可能出现新记录插入”的位置。
- 间隙锁并不直接锁定某一行数据,它锁住的是“禁止插入”的区间,也就是索引 B+ 树中相邻键值之间的空白区域
- 如果没有间隙锁,事务 A 执行
SELECT * FROM t WHERE val BETWEEN 10 AND 20 FOR UPDATE后,事务 B 仍然可以执行INSERT INTO t (val) VALUES (15),那么事务 A 再次执行相同查询时就会多出一条记录,这就是典型的幻读现象 - 严格来说,单独的间隙锁并不是完整解决幻读的手段,它属于
Next-Key Lock的一部分;真正发挥作用的是这种组合锁对“插入意向锁(Insert Intention Lock)”的阻塞能力
哪些查询条件实际会产生间隙锁?
是否会加间隙锁,核心取决于是否发生了范围扫描、索引遍历,或者唯一性校验未命中,而不只是看 SQL 语句表面上写的是等值查询还是范围查询。
- 主键等值查询且记录存在 → 通常只加
Record Lock,没有间隙锁部分 - 主键等值查询但记录不存在 → 会加
Gap Lock,锁住该键值所在的索引间隙,例如(5, 10) - 非唯一索引等值查询 → 即使记录存在,也可能锁住该值对应的多个可能插入位置,因为非唯一索引允许重复值,本质上仍属于间隙覆盖
- 当前读没有走索引 → 全表扫描会退化为锁住整个索引范围,即
(−∞, +∞),实际效果非常接近表级锁
最容易被忽视的 MySQL 实战细节
间隙锁的实际边界,并不是由 SQL 中写出的数值或字符范围直接决定的,而是由底层 B+ 树索引结构决定。比如对于 WHERE name > 'Li' 这条语句,它锁住的是索引中紧邻 'Li' 的后续键值区间,而不是字典序中所有表面上大于 'Li' 的字符串范围。
另外,INSERT ... ON DUPLICATE KEY UPDATE 也不会绕开间隙锁。因为它执行前同样需要先检查唯一索引或主键约束,所以仍然会申请相应的间隙锁,在高并发场景下同样存在较高的死锁风险。
