delete where id between 3 and 7 锁间隙而非行,是因为范围内无匹配记录时,innodb 按 next-key lock 规则锁定索引中左边界前驱(1)与右边界后继(9)之间的开区间(1, 9),该间隙内所有插入均被阻塞。

并发删除同一范围的数据,本质是多个事务在竞争同一个间隙区间,而InnoDB的间隙锁(Gap Lock)不支持共享、不可降级、不可跳过——只要范围重叠,就大概率死锁。
为什么DELETE WHERE id BETWEEN 3 AND 7会锁住间隙而不是行?
当id字段有索引但范围内无实际数据时(比如表里只有id=1和id=9),InnoDB不会加行锁,而是按Next-Key Lock规则向上取整:找到左边界第一个小于3的值(即1),右边界第一个大于7的值(即9),最终锁定开区间(1, 9)。这个区间内所有插入(包括id=4、id=5、id=8)都会被阻塞。
关键点在于:DELETE语句本身不关心“删到了没”,只关心“WHERE条件覆盖了哪些索引位置”;只要扫描路径经过间隙,就按规则加锁。
- 若WHERE条件命中索引但无匹配行 → 加间隙锁
- 若WHERE条件未命中索引(如无索引字段)→ 全表扫描 + 锁全部间隙(更危险)
- 若WHERE条件命中唯一索引且存在对应行 → 只加记录锁(Record Lock),不涉及间隙
saveOrUpdate这类方法在RR隔离级别下怎么悄悄引入死锁?
MyBatis-Plus的saveOrUpdate(T, Wrapper)默认先执行UPDATE,再根据影响行数决定是否INSERT。问题出在第一步:UPDATE若找不到目标记录,在RR级别下会加间隙锁;紧接着的INSERT又需要往同一间隙写入——但此时间隙已被自己(或另一个并发事务)锁住,形成自锁等待链。
典型死锁链:
- 事务A:
UPDATE t SET x=1 WHERE id=4→ 锁(1, 9) - 事务B:
UPDATE t SET x=2 WHERE id=5→ 也锁(1, 9)(注意:间隙锁是共享禁止型,不是共享允许型) - 事务A接着执行
INSERT INTO t(id) VALUES(4)→ 等待(1, 9)释放 - 事务B接着执行
INSERT INTO t(id) VALUES(5)→ 同样等待(1, 9)释放
两个事务互相卡在“等对方释放自己刚申请的锁”,InnoDB检测后回滚其中一个。
线上高频触发死锁的三个隐性条件
不是所有DELETE都容易死锁,但满足以下三点时风险陡增:
- 使用
REPEATABLE READ隔离级别(MySQL默认)→ 间隙锁强制启用 - WHERE条件落在稀疏索引段(如
id IN (1001,1002,1003)但表中只有id=1000和id=1010)→ 实际锁住(1000, 1010)整个大间隙 - 高并发请求携带不同参数但落在同一间隙(如10个线程分别删
id=1001~1009)→ 所有事务争抢同一把间隙锁
最容易被忽略的是第三点:你以为参数不同就互不干扰,其实在间隙锁眼里,它们全挤在同一个“门框”里,谁也进不去。











