如何防止事务系统中的死锁 - DevX
发布时间: November 18, 2025 at 05:11 PM
News Article

内容
如果你曾见过事务系统在高负载下冻结,你就会理解那种悄无声息的恐慌。查询堆积,CPU 使用率看似正常,日志也无异常,但一切都卡住了。死锁是隐秘的问题,直到造成真正损害才显现。它们不会破坏数据,但会杀死系统正常运行时间,动摇对系统的信任,迫使团队争先恐后地预防问题恶化。死锁发生在两个或多个事务相互等待对方持有的锁形成循环时。系统识别该循环并终止其中一个事务以打破循环。在处理大量在线事务的系统中,被终止的事务通常会导致重试、超时和用户看到的连锁失败。因此,防止死锁不仅是性能的附加需求,更是系统韧性的关键。\n\n设计高吞吐量系统的专家一致认为,死锁通常不是随机发生的。Martin Kleppmann 指出,大多数死锁源于锁获取顺序的细微不一致。Pat Helland 认为,将顺序和幂等性视为一等公民的系统很少遇到持续死锁。Tammy Butow 补充说,许多公司低估了因不良退避策略导致的重试风暴加剧死锁的影响。他们的建议归结为三点:可预测的锁顺序、有限的重试次数和清晰的锁授予理解。\n\n那么,死锁为何发生?它们不是意外。通常四个条件同时存在:互斥(一次只有一个事务能持有锁)、持有并等待(事务持有锁同时等待其他锁)、不可抢占(锁不能被强制夺取)和循环等待(事务形成等待锁的循环)。关系数据库设计中包含所有这些条件。开发者主要能控制循环等待。常见原因包括锁顺序不一致、长事务、使用过于严格的隔离级别以及流量高峰时的对抗性访问模式。例如,一个事务先更新行 1 再更新行 2,另一个事务先更新行 2 再更新行 1,当它们重叠时可能发生死锁。\n\n死锁的真正损害不在于单个被终止的事务,而是随之而来的重试风暴。客户端立即无延迟或随机性的重试,导致重试再次碰撞。这可能演变成持续数分钟的故障。一个案例中,支付 API 的 80 毫秒死锁导致五分钟的部分故障,因为重试碰撞严重。在大规模环境中,即使极小比例的死锁也能引发大量失败,饱和连接池。阻止死锁发生远比事后控制风暴容易得多。\n\n防止死锁,首先要强制执行严格且有文档的锁顺序。每种事务类型必须以相同顺序获取锁——按主键、资源类型或层级。不要让用户输入或动态条件改变顺序,否则会引入循环。如果必须有条件写入,先做简单的元数据读取以选择正确的有序路径。\n\n保持事务简短也有帮助。长事务不会直接导致死锁,但增加冲突窗口。移除事务内不必要的查询和昂贵操作如 API 调用。将原子性视为手术刀而非大锤——只保护需要事务保证的部分。\n\n使用合适的隔离级别而非最强隔离。可串行化隔离看似最安全,但通常意味着更多锁和更高死锁风险。选择仍能保证正确性的最弱隔离级别,如读取快照隔离或带显式检查的读已提交。更高隔离不一定更好——只是更严格。\n\n即使如此,仍会发生死锁。因此,始终在重试循环中添加指数退避和抖动。失败后不要立即重试。随机延迟避免重试集中碰撞。即使是小抖动也能大幅减少重试碰撞,常常能避免小问题演变成重大事故。\n\n最后,跟踪锁争用作为关键指标。大多数团队关注整体查询延迟,却忽视查询等待锁的时间。测量阻塞时间、死锁计数和主要锁等待者,有助于及早发现问题,防止故障发生。
关键见解
提取的核心事实强调,死锁源于事务系统中固有的循环等待条件,尽管不破坏数据,却导致重大运营中断。
关键利益相关者包括数据库工程师、SRE 团队、应用开发者及依赖系统正常运行的终端用户。
次级影响波及依赖稳定事务处理的业务部门。
即时后果表现为重试风暴和连锁失败,常因退避策略管理不善而加剧。
历史案例中,支付处理系统的故障因重试碰撞放大初始死锁,延长停机时间。
对比过去事件凸显严格锁顺序和带抖动重试作为有效缓解措施的重要性。
展望未来,乐观情景包括动态锁管理和自适应重试算法的工具改进,风险则是扩展挑战超出现有防范手段。
监管机构和技术负责人应优先执行标准化锁获取协议,强制监控锁争用作为主要指标,并实施结构化重试退避策略。
这些建议在实施复杂度与系统韧性显著提升之间取得平衡,确保死锁被主动管理而非被动应对。