间隙锁仅锁定索引记录间的空隙(如(3,5)),不锁任何现有记录,专用于防止幻读;在repeatable read隔离级别下生效,read committed下不启用。

Gap Lock 只锁间隙,不锁记录
Gap Lock 的作用非常明确:防止其他事务在某个索引区间内插入新数据,从而避免幻读。它**不锁定任何现有记录**,只锁定两个索引值之间的“空隙”。比如表中 v1 索引有值 3 和 5,那么 (3, 5) 就是一个间隙,INSERT INTO t(v1) VALUES(4) 会被阻塞。
常见触发场景:
- 非唯一索引上的等值查询未命中(如
SELECT * FROM t WHERE v1 = 4 FOR UPDATE,但表中没有v1 = 4的行) - 范围查询中,扫描到的最后一个索引值不满足条件(如
WHERE v1 > 7,而最大v1是6) - 显式使用
SELECT ... FOR UPDATE或UPDATE/DELETE且走非唯一索引时,间隙部分单独被锁
注意:Gap Lock 在 READ COMMITTED 隔离级别下**完全不启用**,这也是为什么 RC 级别会出现幻读——它靠 MVCC + 行锁解决并发,但不拦插入。
Next-Key Lock 是 Gap Lock + Record Lock 的组合
Next-Key Lock 不是新类型的锁,而是 InnoDB 在 REPEATABLE READ 隔离级别下的**默认加锁行为**:对匹配到的每条索引记录加 Record Lock,同时对其左边的间隙加 Gap Lock,合起来就是左开右闭区间,例如 (3, 5]。
关键细节:
- 锁定范围是「左开右闭」:包含右端点记录,不包含左端点记录
- 退化规则很重要:唯一索引 + 等值查询(如
WHERE id = 5)会退化为纯Record Lock,不加间隙部分 - 非唯一索引 + 等值查询(如
WHERE v1 = 5)则仍为 Next-Key Lock,即锁(3, 5](假设有v1=3) - 范围查询(如
WHERE v1 BETWEEN 3 AND 7)会覆盖多个区间,锁住所有涉及的(x, y]
你可以用 SELECT * FROM performance_schema.data_locks 查看当前事务持有的锁类型和范围,其中 LOCK_MODE 字段显示 RECORD、GAP 或 NEXT-KEY。
为什么区分二者对排查死锁至关重要
很多线上 Error 1213 (40001): Deadlock found when trying to get lock 并不是因为更新了同一行,而是两个事务在**不同但重叠的间隙上加锁**,又互相尝试插入对方间隙中的值。
典型死锁链:
- Session A 执行
SELECT * FROM t WHERE v1 = 5 FOR UPDATE→ 加 Next-Key Lock(3, 5] - Session B 执行
SELECT * FROM t WHERE v1 = 3 FOR UPDATE→ 加 Next-Key Lock(1, 3] - Session A 尝试插入
v1 = 4→ 需要(3, 5)间隙锁,但已被 A 自身的 Next-Key 锁覆盖?不,问题在 B 也持有(1, 3],而v1 = 4不冲突;真正冲突的是:A 想插v1 = 2(需(1, 3)),B 想插v1 = 4(需(3, 5))→ 两者分别等待对方释放对方所需的间隙
这种死锁不会出现在唯一索引等值更新场景中,因为那里只有 Record Lock,不扩散到间隙。所以一旦遇到难以复现的死锁,先确认语句是否走了非唯一索引、是否用了范围条件、事务隔离级别是不是 RR —— 这三点几乎决定了锁的“攻击半径”。
实战中如何避免 Gap/Next-Key 锁引发的问题
不是所有场景都需要消灭它们,但得知道怎么收窄影响:
- 写入逻辑尽量走主键或唯一索引的等值条件,避免触发间隙锁扩散
- 业务允许时,把隔离级别降为
READ COMMITTED,Gap Lock 全部失效(但要接受幻读语义) - 批量插入前先
SELECT ... FOR UPDATE占位,但必须确保查询条件能精确覆盖待插范围,否则可能锁得更宽 - 避免在高频更新的非唯一索引字段上做
FOR UPDATE范围查询;可考虑改用主键分批处理
最常被忽略的一点:即使 SQL 看似只查一行,只要走的是非唯一索引,InnoDB 仍可能加 Next-Key Lock —— 因为它无法提前确认“这一行是否唯一”,只能按规则保守加锁。看执行计划不够,得看 data_locks 实际持有哪些锁。











