第 8 / 22 篇

第 7 章:死锁后,重试的是完整业务事务

两个事务分别先更新账户 1 与账户 2,再尝试更新对方已经锁住的账户。双方都无法继续,形成等待环。InnoDB 默认开启死锁检测,通常会选择一个受害事务并回滚,让另一个继续。

7.1 用相反锁序制造真实竞争

实验的两个线程都先成功完成第一条 UPDATE,再通过同步屏障进入第二条。这样不依赖猜测某个 sleep 足够长,能够稳定制造竞争窗口。

最终一个事务提交,另一个收到 1213。账户初始为 100,获胜事务分别增加两行,最后都是 101;受害事务的第一条修改没有残留。这是整笔事务被回滚的证据。

为了区分引擎自动回滚与应用主动清理,这个隔离实验在捕获 1213 后故意不调用 ROLLBACK,而是再发送 COMMIT。若第一条修改仍被保留,它就可能被提交,最终余额不会是 [101,101]。这是验证回滚范围的探针,不是建议生产应用在异常后继续提交业务;实际恢复仍应显式结束当前事务、重新读取并按完整业务单位处理。

A: 锁账户1,增加1
B: 锁账户2,增加1
A: 等待账户2
B: 等待账户1
InnoDB: 回滚一个受害事务
另一个事务继续并提交

哪个事务被选为受害者不是应用应依赖的稳定顺序。测试检查一份提交、一份 1213 和最终余额,没有规定某个线程必须获胜。

7.2 不只重发报错的那一条

受害事务之前的读取和写入已被回滚。只重试最后一条 UPDATE,可能丢掉整个业务过程的前半部分。正确的恢复单位是经过重新读取与校验的完整业务事务,并且仍应受到次数、期限和资源预算限制。

业务事务如果包含事务外副作用,重试还可能重复发送通知或调用支付。这些动作应通过业务幂等与事件机制安排,不能仅靠数据库回滚消除。

7.3 统一顺序缩小等待环的机会

多个事务需要锁同一组账户时,可以按稳定的账户 ID 顺序获得锁。缩短事务、减少持锁期间的外部等待,以及为重要条件提供适当索引,也有助于降低竞争。

这些做法减少风险,不构成“以后不会再死锁”的承诺。范围、多个表和不同访问路径仍可能产生复杂竞争;应用必须保留死锁处理与观察能力。

7.4 看诊断,也看业务结果

SHOW ENGINE INNODB STATUS 可以帮助观察最近的 InnoDB 死锁。若要持续收集,还应按部署策略管理错误日志与相关配置。诊断记录中的 SQL、对象和连接可能含业务信息,需要控制访问与保留。

最终是否修复,应以真实并发测试和业务不变量判断。死锁次数下降,却把事务拆成互不原子的几段,可能让面板更安静但业务更容易出现半完成。

练习。 转账程序扣款后发送通知,再写入收款记录。若它因为死锁重试整个函数,通知可能发生什么?请重新安排事务与通知边界,并说明仍需哪项幂等保证。