死锁检测、超时与重试
数据库死锁发生在多个事务形成循环等待时。InnoDB 可以检测等待图中的环并回滚一个事务,但业务仍需正确处理失败、重试和可能已经发生的外部副作用。
1. 相反的加锁顺序会形成循环
事务 A 先锁订单 1,再等待订单 2;事务 B 先锁订单 2,再等待订单 1。两边都无法继续,也不会仅靠其中一个锁正常释放而结束等待。
事务 A:持有 order:1 → 等待 order:2
事务 B:持有 order:2 → 等待 order:1
死锁不仅来自两条显式 UPDATE。二级索引与聚簇索引的访问顺序、范围锁、外键检查和插入唯一键冲突也可能参与等待环。
2. InnoDB 检测等待图并选择牺牲者
启用死锁检测时,InnoDB 会检查事务等待关系是否形成环。发现死锁后,它选择一个事务作为 victim 回滚,让其他事务继续。通常会倾向回滚修改较少、回滚成本较低的事务,但应用不能依赖某个固定事务一定获胜。
收到死锁错误表示当前事务已经失败。继续在同一业务流程中假设前面的修改有效,会破坏状态判断。
3. 锁等待超时与死锁不是一回事
锁等待超时表示某个锁在配置时限内没有取得,等待关系未必形成环。默认情况下,InnoDB 的锁等待超时通常只回滚当前语句;死锁检测则会回滚整个事务。具体行为还会受配置影响。
应用处理时不应依靠这个细节继续拼接事务。最安全的边界是让事务方法整体失败并回滚,再由明确的上层策略决定是否重试完整业务事务。
延长锁等待超时不会消除死锁,只会让普通阻塞占用连接更久。超时应服从接口总预算。
4. 重试要重新执行完整事务
死锁通常是并发执行顺序造成的瞬时失败,适合有限重试。一次重试应:
- 结束并回滚原数据库事务。
- 重新读取当前数据和前置条件。
- 从事务入口重新执行业务逻辑。
- 使用指数退避和随机抖动错开竞争。
- 设置最大次数,并在总 deadline 内停止。
只重放最后一条 SQL 可能遗漏事务前面基于旧数据做出的判断。立即无限循环重试则会在热点冲突中进一步增加负载。
5. 数据库事务外的副作用需要幂等
如果事务中间已经发送 HTTP 请求、投递不可撤回的消息或调用第三方支付,数据库回滚不能撤销这些动作。重试整个方法可能再次执行副作用。
应把远程调用移出持锁事务,并通过幂等键、Outbox、状态机或补偿流程协调。数据库事务只覆盖能由同一资源管理器原子提交的变化。
6. 减少死锁依赖访问顺序和事务长度
常见控制方法包括:
- 所有代码按同一顺序访问同类资源,例如始终按主键升序更新。
- 为过滤条件建立合适索引,减少扫描和锁定范围。
- 缩短事务,避免持锁期间执行网络调用和耗时计算。
- 把大批量更新拆成可恢复的小批次。
- 对热点资源使用条件更新、分区所有权或串行化队列。
无法通过“按顺序加锁”覆盖所有索引内部行为,所以仍要保留正确的失败与重试处理。
7. 从死锁记录还原两条执行链
SHOW ENGINE INNODB STATUS 会保留最近一次死锁信息,包括参与事务、持有和等待的锁以及相关 SQL。还可以使用 Performance Schema 查看当前 data_locks 和 data_lock_waits。
排查步骤是:
- 找出每个事务已经持有什么锁、正在等待什么锁。
- 确认锁属于哪个索引和记录或区间。
- 回到完整事务代码,找出此前执行的 SQL 和调用顺序。
- 检查执行计划与数据分布是否扩大了扫描范围。
- 用并发测试复现,并验证修改后死锁率和延迟。
只保留最后一条报错 SQL 往往无法还原循环,因为造成锁顺序的语句可能更早执行。
8. 常见问题
8.1 发生死锁是否说明数据库有故障
不一定。死锁是并发事务允许等待时可能出现的结果,数据库检测并打破它属于正常恢复机制。频繁死锁则说明访问顺序、索引、事务边界或热点模型需要调整。
8.2 使用悲观锁能否避免死锁
不能。悲观锁让冲突更早显式发生,但多个事务仍可能以不同顺序取得多把锁。统一顺序、缩短事务和正确重试仍然必要。
9. 面试题
9.1 数据库发生死锁后应该如何定位和处理
出现公司:字节跳动
考察重点
- 循环等待、死锁检测和 victim 回滚。
- 死锁与锁等待超时的区别。
- 完整事务重试、幂等和事务外副作用。
相关内容:第 1 节“相反的加锁顺序会形成循环”至第 7 节“从死锁记录还原两条执行链”。
参考回答
死锁是多个事务各自持有锁并等待对方资源,形成循环等待。InnoDB 会检测等待图并回滚一个事务,让其他事务继续;锁等待超时则只表示等待超过时限,不一定有环,两者的回滚行为也可能不同。
应用收到死锁后应回滚原事务,在总超时内用退避和抖动有限重试完整事务,并重新读取数据。定位时从 InnoDB deadlock 信息和 Performance Schema 找出双方持有、等待的索引锁,再还原完整 SQL 顺序和执行计划。长期治理包括统一资源访问顺序、补充索引、缩短事务,以及让事务外副作用具备幂等或 Outbox 边界。