DELETE触发死锁本质是多事务竞争同一资源时加锁顺序不一致导致循环等待,InnoDB复用SELECT FOR UPDATE机制加next-key lock,索引不当、全表扫描或混合操作均会加剧风险。

DELETE 语句为什么会触发死锁
MySQL 的 DELETE 本身不特殊,它复用 SELECT ... FOR UPDATE 的加锁逻辑:先走索引定位行,再对匹配记录加 next-key lock(记录锁 + 间隙锁)。只要两个事务扫描的索引范围有重叠,且加锁顺序相反,就可能形成循环等待。
典型现象是应用日志里突然冒出 Deadlock found when trying to get lock; try restarting transaction(错误码 1213),而对应 SQL 正是 DELETE FROM t WHERE status = ? AND created_at 这类带条件的删除。
- WHERE 条件没走索引 → 全表扫描 → 锁住大量无关行,与其他事务交叉覆盖
- 复合索引字段顺序和 WHERE 条件不匹配(比如索引是
(status, created_at),但查询写了WHERE created_at )→ 优化器可能退化为范围扫描或索引失效 - 事务中混用
UPDATE和DELETE,且它们走的索引不同(一个走PRIMARY,一个走idx_status)→ 加锁路径分裂 - 高并发下相同 WHERE 条件的
DELETE被多个事务执行,但因统计信息陈旧或并发读取导致优化器选了不同索引 → 实际加锁顺序不一致
怎么用 SHOW ENGINE INNODB STATUS 定位根因
死锁发生后,MySQL 自动回滚一个事务,并把上下文写进 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 区域。关键不是看“谁被回滚”,而是比对两个事务的三处细节:
-
事务持有的锁:关注
lock_mode X locks rec but not gap(行锁)或lock_mode X locks gap before rec(间隙锁),确认锁在哪几行、哪个索引上 -
事务正在等待的锁:看
waiting for this lock to be granted后面的space id、page no、rec no,它指向具体记录位置 -
SQL 执行路径:对比两个事务的
mysql tables in use和index字段 —— 如果一个显示index `idx_status`,另一个是index `PRIMARY`,说明本该走同一索引却走了不同路径,这是最常见的人为诱因
示例中若事务 A 显示 index `idx_status_created`,事务 B 却显示 index `PRIMARY`,大概率是 WHERE 条件中用了未被该二级索引覆盖的列(比如 WHERE status = 1 AND user_id = 100,但索引只有 (status, created_at)),导致回表时锁了主键范围。
加了索引为什么还死锁
加索引只是前提,真正决定死锁概率的是「是否索引覆盖」和「索引字段顺序是否匹配查询模式」。InnoDB 在 DELETE 时仍需回表校验可见性,如果 WHERE 条件涉及的列不在索引中,就会先走二级索引定位,再通过主键回查——这个过程会引入额外的锁区间,尤其是间隙锁。
- 索引存在但未被使用:检查
EXPLAIN DELETE ...,确认key字段是否非 NULL,type是否为ref或const,而非range或ALL - 索引覆盖不全:比如
DELETE FROM order WHERE status = 'pending' AND amount > 100,若索引只建在(status),amount条件就得回表,可能锁住主键区间 - 间隙锁不可避:在
REPEATABLE READ隔离级别下,WHERE status = 'pending'这种等值查询,若status不是唯一索引,InnoDB 仍会加 next-key lock,锁定「等于 pending 的所有行 + 后续间隙」
应用层必须做的三件事
MySQL 不会替你设计事务逻辑,死锁错误(1213)必须由应用捕获并主动应对。不能靠调大 innodb_lock_wait_timeout 拖延问题。
-
必须捕获错误码 1213:ORM 如 MyBatis、GORM 默认不重试死锁,需显式 catch
sql.ErrNoRows之外的底层错误,判断errno == 1213 - 幂等重试要带退避:简单 while 循环重试可能加剧竞争;建议用指数退避(如 10ms → 30ms → 100ms),最多 3 次
-
删除前先 SELECT FOR UPDATE(慎用):适用于业务能接受“先查再删”且数据量可控的场景;但若
SELECT ... FOR UPDATE范围过大,反而扩大锁粒度,得不偿失
最容易被忽略的一点是:死锁往往不是单条 SQL 的问题,而是多个业务模块共用同一张表、各自按自己理解建索引和写 WHERE 条件。协调索引设计和查询规范,比调参重要得多。


















