InnoDB通过wait-for graph算法实时检测死锁,加锁失败瞬间建图并发现环,微秒级响应;选权重最小(修改行数+语句数)事务回滚,权重相同时选等待时间更短者;应用须捕获ERROR 1213并重试事务。

死锁检测靠的是 InnoDB 的 wait-for graph 算法
MySQL(准确说是 InnoDB 存储引擎)在事务执行过程中,会实时维护一个“事务等待图”:每个节点是事务,每条有向边 T1 → T2 表示 T1 正在等待 T2 持有的锁。一旦图中出现环,就判定为死锁。
这个检测不是定时轮询,而是发生在每次加锁失败、进入等待队列前的瞬间。也就是说,死锁几乎在形成的同时就被发现,延迟通常在微秒级。
- 检测触发点严格限定在行锁(record lock)、间隙锁(gap lock)或临键锁(next-key lock)的获取阶段
- 表级锁(如
LOCK TABLES)不参与该机制,也不会被检测为死锁 - 只对持有锁且正在等待锁的活跃事务建模,已提交或已回滚的事务不纳入图中
死锁回滚由 InnoDB 自动选择牺牲者
检测到环后,InnoDB 必须选一个事务回滚来打破循环。它不会随机选,而是基于“事务权重(weight)”做决策:权重越小,越可能被回滚。
权重计算规则是:已修改的行数 + 已执行的 SQL 语句数。换句话说,改动越少、越“轻量”的事务优先被干掉。
- 如果两个事务权重相同,InnoDB 会选
wait-for graph中更晚加入等待队列的那个(即等待时间更短的) - 被选中的事务收到错误:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction - 回滚是完整事务级回滚,不是只撤回某条语句;所有该事务此前的 DML 都失效
如何复现和验证死锁行为
最可靠的方式是用两个客户端(或连接)按相反顺序更新同一组主键行,比如:
-- 连接 A START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 不提交,保持事务打开 <p>-- 连接 B START TRANSACTION; UPDATE accounts SET balance = balance + 100 WHERE id = 2; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 此时阻塞,等待 A 的锁</p><p>-- 回到连接 A UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 此时触发死锁检测
此时至少一方会立刻收到 ERROR 1213。可通过 SHOW ENGINE INNODB STATUS\G 查看最近一次死锁详情,其中 LATEST DETECTED DEADLOCK 段落会明确列出两个事务的 SQL、持有的锁、等待的锁及最终被回滚的事务 ID。
应用层必须捕获并重试死锁异常
InnoDB 不会重试,它只负责报错。业务代码若不处理 ERROR 1213,就会让请求失败。这是最容易被忽略的一环。
- 不能把死锁当成“偶发错误”忽略,而应视作正常并发现象,写重试逻辑
- 重试次数建议限制在 2–3 次,避免无限循环;每次重试前加随机短延时(如 10–100ms)降低再次冲突概率
- 注意:重试必须重新开始整个事务(
BEGIN),不能接着上一次未完成的状态继续 - 某些 ORM(如 SQLAlchemy、MyBatis)提供死锁重试插件,但底层仍是捕获
1213错误码再封装
真正难的从来不是理解机制,而是意识到:死锁检测本身很快,但业务没做重试,才让问题从“瞬时可恢复”变成“请求失败”。










