delete在rr隔离级别下复用select for update的next-key lock机制,非唯一索引、范围条件、无索引或联合索引未按最左前缀查询均会触发间隙锁膨胀;主键等值删除是否锁间隙取决于值是否存在及索引类型;“空删”亦可持有gap lock导致insert阻塞。

DELETE在RR隔离级别下默认复用SELECT FOR UPDATE加锁逻辑
MySQL的DELETE语句在REPEATABLE READ隔离级别下,并不单独实现一套加锁规则,而是直接复用SELECT ... FOR UPDATE的锁机制。这意味着只要语句是“当前读”,就按Next-Key Lock(记录锁 + 间隙锁)处理——不是DELETE自己“多此一举”,而是RR级别保障可重复读和防幻读的底层契约。
所以哪怕你写的是DELETE FROM t WHERE id = 100,只要id不是唯一索引、或该值不存在、或优化器没走索引,InnoDB就会加Next-Key Lock,而Next-Key Lock天然包含间隙部分。
非唯一索引、范围条件、无索引都会触发间隙锁膨胀
以下任一情况,DELETE就会锁住远超目标行的间隙:
-
WHERE字段是普通索引(如status),即使值存在,也会对索引项及其前隙加Next-Key Lock,比如锁定(95, 100] - 用了范围条件:
WHERE created_at > '2025-01-01'、WHERE id BETWEEN 100 AND 200、WHERE name LIKE 'abc%' - 条件字段没索引,或索引失效:如
WHERE YEAR(created_at) = 2025、WHERE status + 0 = 1→ 触发全表扫描,每行都加Next-Key Lock,等效锁表 - 联合索引未按最左前缀使用:索引是
(a, b, c),却查WHERE b = 5,无法定位起始点,只能扫整个a值分组,间隙锁范围爆炸
主键等值删除也不一定只锁一行
很多人以为DELETE FROM t WHERE id = 100(id为主键)必然只锁一行,但实际取决于三件事:
- id是主键或唯一索引,且100存在 → 加Record Lock,不锁间隙
- id是主键,但100不存在 → 加Gap Lock,锁前一个存在的主键值到后一个之间的间隙,例如现有95和105,则锁
(95, 105) - id是普通索引(哪怕叫id)→ 即使100存在,也加Next-Key Lock,连带封锁
(prev_id, 100]整段
注意:SHOW ENGINE INNODB STATUS里看到lock_mode X locks rec but not gap只说明记录锁部分,不反映间隙锁是否已加;真正要确认锁范围,得结合EXPLAIN看执行计划 + 并发INSERT验证是否被阻塞。
死锁与阻塞常源于Gap Lock不可见但真实存在
最易被忽略的一点:DELETE即使没删到任何行,也可能持有Gap Lock。比如表中id为10、20、30,执行DELETE FROM t WHERE id = 15,InnoDB会锁(10, 20)。此时另一个事务想INSERT INTO t VALUES (15, ...),就会申请插入意向锁,而该锁与Gap Lock互斥 → 直接等待。
这种“空删”引发的锁,不会出现在慢日志里,也不会报错,但会导致其他INSERT莫名卡住。排查时不能只盯waiting for insert intention lock,而要查information_schema.INNODB_TRX找活跃事务,再用performance_schema.data_locks(MySQL 8.0+)过滤LOCK_MODE = 'GAP'或'NEXT-KEY',看LOCK_DATA是否覆盖你要插入的值。
真正难处理的,从来不是删得多,而是删得“看起来安全”,实则锁了一大片空隙——尤其当业务依赖时间字段清理旧数据,又没建好(status, create_time)这类覆盖索引时。











