跨库事务死锁自动恢复:锁等待超时回滚失败?分布式死锁检测 + 智能重试机制!

朋友公司的订单系统连了两个库——订单库和库存库。一笔订单创建需要在订单库 INSERT、在库存库 UPDATE,同一个事务里跨两库操作。高峰期出现了死锁:事务 A 锁了订单表的行等库存表的锁,事务 B 锁了库存表的行等订单表的锁。两个事务互相等待,40 秒后被 MySQL 的 innodb_lock_wait_timeout 强杀。问题是强杀后抛出的异常只说了"锁等待超时",没说是谁锁了谁——运维排查不到哪个事务导致了死锁。

单库死锁 MySQL 自己能检测和回滚,跨库死锁 MySQL 管不了——因为在它看来就是两个独立的事务各等各的。今天聊聊怎么在应用层做跨库死锁检测,并配合智能重试自动恢复。


跨库死锁是怎么发生的

单库死锁 MySQL 的 InnoDB 引擎自己就能处理:检测到环 → 选一个代价最小的回滚。但跨两个库的时候,场景是这样的:

事务 A:                      事务 B:
  BEGIN                        BEGIN
  UPDATE orders SET ...        UPDATE inventory SET ...
  WHERE id=1001                WHERE sku='XYZ'
  (锁住订单库的行)            (锁住库存库的行)
                               
  UPDATE inventory SET ...     UPDATE orders SET ...
  WHERE sku='XYZ'              WHERE id=1001
  (等库存库的锁 ←→            (等订单库的锁)
   事务 B 拿着不放)              事务 A 拿着不放)

MySQL 只看自己的库,看不到别的库。在它眼里这不是死锁,只是"两个事务各等各的"。等到 innodb_lock_wait_timeout 到期,各自回滚。问题是这个超时通常设得比较长(默认 50 秒),线上服务扛不住这么久的等待。


分布式死锁检测:应用层建一张"等锁图"

既然 MySQL 不帮你检测,就得自己在应用层做。核心思路:维护一个全局的"谁在等谁"关系图,发现环就把环上代价最小的事务杀掉。

应用层做检测需要知道两件事:每个事务当前锁了哪些资源、每个事务正在等待哪个资源。

实现上可以在每次执行 SQL 前后记录锁资源:

事务 A:
  acquire("order:1001")     → 记录:A 持有 order:1001
  acquire("inventory:XYZ")  → 记录:A 等待 inventory:XYZ
  (发现 B 持有 inventory:XYZ) → 记录:A 等待 B

事务 B:
  acquire("inventory:XYZ")  → 记录:B 持有 inventory:XYZ
  acquire("order:1001")     → 记录:B 等待 order:1001
  (发现 A 持有 order:1001) → 记录:B 等待 A

死锁检测器:
  A → B → A 形成环 → 死锁!
  选择 A 或 B 中代价小的那个回滚

伪代码:

class DistributedDeadlockDetector:
    # key → 持有它的事务
    lockHolders: Map

    # 事务 → 它在等哪个 key
    waitFor: Map

    detect():
        # 构建等待图并检测环
        for each transaction waiting:
            if 存在环:
                victim = chooseVictim(环上事务)
                victim.rollback()
                通知其他事务可以继续

不过生产环境不一定要做到这种粒度。一个更务实的做法是:给跨库操作加一个全局的分布式锁(Redis),扣款和扣库存串行化。死锁直接不发生,比检测+恢复更简单可靠。


智能重试:回滚了别白费

死锁检测到并回滚只是第一步。这笔订单还得继续处理——不能因为一次死锁就丢了。

重试的关键:不是简单循环再调一次,而是要避开导致死锁的资源竞争点。

智能重试的策略:

1. 回滚当前事务,释放所有锁
2. 记录重试次数 + 1
3. 计算退避时间:指数退避 + 随机抖动
   - 第 1 次:等 100ms
   - 第 2 次:等 200ms
   - 第 3 次:等 400ms
4. 重试次数超过上限(比如 3 次)→ 人工介入
5. 加上随机抖动(0~50ms),让冲突的事务各自错开

随机抖动是关键——两个冲突的事务如果同时重试、同时再锁同样的资源,大概率再次死锁。加上随机等待时间,让它们在时间上错开,打破死锁条件。


更简单的方案:排序加锁避免死锁

如果你不想搞死锁检测这么重,还有一个治本的办法:保证所有跨库事务的加锁顺序一致。

死锁的发生需要四个条件之一"循环等待"。如果所有事务都按"先锁订单库再锁库存库"的固定顺序来,循环就不可能发生。

所有事务统一顺序:
  第 1 步:锁订单库的资源
  第 2 步:锁库存库的资源
  第 3 步:执行业务逻辑
  第 4 步:提交

这个方案不需要死锁检测、不需要重试逻辑,唯一的代价是"必须按固定顺序写代码"。团队里如果有人图方便把顺序写反了,死锁又会回来。所以需要代码评审或者检查工具兜底。


总结

跨库死锁的本质是 MySQL 管不到别的库。解决方案三个层次:

  • 最简单的:排序加锁——所有事务统一先 A 后 B,死锁不发生
  • 中等复杂度:全局锁串行化——Redis 分布式锁保底,牺牲一点并发换可靠性
  • 最完整的:分布式死锁检测 + 智能重试——应用层建等锁图,检测到环就回滚 + 指数退避重试

生产环境建议从方案一开始,做不到就上方案二。方案三是兜底——当你的跨库事务逻辑复杂到没法统一加锁顺序的时候,它能救你一命。


有用的话转给还在被 MySQL lock wait timeout 折磨的同事。


标题:跨库事务死锁自动恢复:锁等待超时回滚失败?分布式死锁检测 + 智能重试机制!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/06/14/1781421584513.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消