delete触发死锁本质是多事务竞争同一资源时加锁顺序不一致导致循环等待,innodb复用select for update机制加next-key lock,索引缺失、间隙锁、跨表删序不一致及长事务共同构成死锁环。

高并发下 DELETE 触发死锁,不是语句写错了,而是锁等待路径形成了环——必须从执行顺序、索引覆盖和事务粒度三处同时干预,单点优化大概率无效。
为什么 DELETE 会卷入死锁环?
很多人以为 DELETE 只锁要删的行,实际在 InnoDB 中它可能锁住更多:
- 没走索引时,
DELETE FROM t WHERE status = 'expired'会升级为全表扫描+全表加锁,所有并发DELETE都在争同一把表级隐式锁 - 走了非唯一索引但条件不精确(如
WHERE create_time ),InnoDB 会加间隙锁(Gap Lock),把“本不该删但可能插入的位置”也锁住,导致不同事务的删除范围重叠、互相阻塞 - 多个事务先删 A 表再删 B 表,而另一批事务反向操作(先 B 后 A),
DELETE本身不冲突,但和后续其他 DML 组合后就构成循环等待链
DELETE 语句必须带有效索引
索引不是可选优化项,是避免锁升级的硬性前提。验证方法很简单:对目标 DELETE 执行 EXPLAIN,确认 type 是 range 或 ref,且 key 显示用了具体索引名。
- 如果
WHERE条件含函数(如DATE(created_at) = '2025-01-01'),索引失效,改用created_at >= '2025-01-01' AND created_at - 复合索引要注意最左前缀:若建了
INDEX idx_status_time (status, created_time),但查询只用WHERE created_time ,该索引不会被选中 - 对时间范围删除,优先建
(status, created_time)而非单独created_time,避免状态过滤后仍需回表判断
批量 DELETE 必须分片 + 有序 + 限流
一次性删 10 万行,等于让一个事务持锁几十秒,其他所有相关操作全部排队——这不是删除,是主动制造雪崩。
- 用
LIMIT分批,但别写成DELETE ... LIMIT 1000就完事;必须配合WHERE id > ?这类游标式条件,防止重复删或漏删 - 所有分批任务按主键或时间字段单调递增执行(如
WHERE id BETWEEN 1000 AND 2000),确保不同线程的锁区间不交叉 - 应用层控制并发数,比如用信号量限制最多 3 个线程同时删同一张表,避免瞬间涌入大量锁请求
死锁发生后不能只靠重试
捕获 ERROR 1213 (40001): Deadlock found when trying to get lock 后立即重试,能解决 70% 场景,但掩盖了根本问题:
- 重试逻辑必须带指数退避(如第 1 次延时 10ms,第 2 次 20ms,第 3 次 40ms),否则重试风暴会让死锁更频繁
- 每次重试前检查是否还有残留锁:查
SELECT * FROM information_schema.INNODB_TRX,确认事务已真正回滚,而非卡在 waiting 状态 - 线上必须开启
innodb_print_all_deadlocks = ON,把死锁现场写进错误日志;否则你永远不知道是哪两条DELETE在打架
真正难处理的,是那些看似合理、索引正确、分批执行,却因业务逻辑交叉(比如删订单前要更新用户积分)而意外形成锁环的情况——这时得靠 SHOW ENGINE INNODB STATUS 里 LATEST DETECTED DEADLOCK 段落,逐行比对 SQL 和锁类型,才能定位到那个被忽略的隐式依赖。










