delete对相邻记录加gap lock,根本原因是rr隔离级别下innodb对索引中“应处”间隙无差别加锁:如id=10和20存在,则delete where id=15(无论是否存在)必锁(10,20);主键等值删除是否锁相邻,取决于值是否存在及索引类型。

DELETE为什么会对相邻记录加Gap Lock
根本原因不是“删多了”,而是InnoDB在REPEATABLE READ隔离级别下,只要WHERE条件走索引(哪怕没命中),就会对索引中该值“应处”的间隙加Gap Lock。这个间隙由前后存在的索引值界定,比如索引中有id = 10和id = 20,执行DELETE FROM t WHERE id = 15时,即使表里根本没有id = 15的行,也会锁住开区间(10, 20)——这正是相邻记录定义的边界。
主键等值删除也锁相邻记录?看这三种情况
很多人以为DELETE FROM t WHERE id = 100(id为主键)只锁一行,实际是否如此,取决于值是否存在、索引类型和存储结构:
- id是主键或唯一索引,且
100存在 → 只加Record Lock,不锁间隙 - id是主键,但
100不存在 → 加Gap Lock,锁(prev_id, next_id),例如前后是95和105,就锁(95, 105) - id是普通索引(非唯一)→ 即使
100存在,也会加Next-Key Lock,即Record Lock + Gap Lock,锁住(prev_id, 100],把前一个值到当前值之间的所有空位一并封死
复合索引下“看似点查”实则范围扫描
建了(a, b, c)联合索引,却写WHERE b = 1 AND c = 2,优化器无法使用最左前缀,往往退化为全索引扫描或范围扫描,导致锁住整个a值对应的所有间隙段。这不是“误锁”,而是InnoDB按执行计划老老实实扫、老老实实加锁的结果。
验证方式很简单:把DELETE语句改成SELECT *,跑一次EXPLAIN:
-
type = ALL或key = NULL→ 几乎肯定触发全表扫描+大量Next-Key Lock -
type = range但rows远大于预期 → 实际锁住的间隙可能远超目标行 -
Extra里出现Using index condition或Using where→ 表明索引部分生效,但仍可能因过滤延迟导致锁范围扩大
DELETE未命中反而更危险:隐式Gap Lock持有者
没删到任何行的DELETE,常常是最难排查的锁源。它不修改数据,但提前占用了插入意向锁必需的间隙,而INSERT必须申请Insert Intention Lock才能写入——这个锁与Gap Lock互斥。
典型死锁链:
- 事务A执行
DELETE FROM t WHERE id = 15(无此行)→ 锁(10, 20) - 事务B执行
INSERT INTO t VALUES (15, ...)→ 在(10, 20)申请插入意向锁 → 被A阻塞 - 事务C也执行
DELETE FROM t WHERE id = 16(同样无此行)→ 也锁(10, 20)→ B和C互相等待对方释放间隙 → 死锁
这种“空删”行为在高并发批量清理场景中极易复现,且SHOW ENGINE INNODB STATUS里只显示locks gap before rec,不提具体SQL,容易被忽略。











