数据库死锁故障分析复盘
1. 故障现象
系统在执行数据库写入时出现:
Deadlock found when trying to get lock; try restarting transaction数据库检测到多个事务之间存在循环等待,主动回滚其中一个事务,并将死锁异常返回给应用程序。
需要注意:异常堆栈中显示的 SQL,只代表数据库发现死锁的位置,不一定是最先持有锁、导致死锁的 SQL。
2. 死锁原因分析
2.1 死锁形成的必要条件
数据库死锁通常需要同时满足以下条件:
- 存在两个或以上并发事务;
- 事务访问了相同的记录、唯一索引、普通索引或间隙;
- 每个事务都持有部分锁,同时等待其他事务持有的锁;
- 锁的获取顺序不一致,形成循环等待。
如果只有一个事务,或者多个事务只访问互不重叠的数据,通常只会出现锁等待,不会形成死锁。
2.2 典型死锁过程
假设两个事务需要处理相同的两条记录:
事务 T1:记录 A -> 记录 B事务 T2:记录 B -> 记录 A可能的执行顺序为:
T1 获取记录 A 的锁T2 获取记录 B 的锁T1 申请记录 B 的锁,等待 T2T2 申请记录 A 的锁,等待 T1等待关系为:
T1 -> T2 -> T1这就是完整的死锁环。数据库会选择其中一个事务作为牺牲者回滚,另一个事务继续执行。
2.3 批量写入为什么容易产生死锁
批量写入通常会在一个事务中处理多条记录。事务在执行过程中会逐步获取多条记录或索引锁,并且这些锁要等事务提交后才释放。
当两个批次存在交集,且数据顺序不一致时,就可能出现:
批次 1:A、B、C批次 2:C、B、A批量 Upsert 也不能天然避免死锁。即使 SQL 使用了“重复键更新”,数据库仍然需要:
- 检查唯一索引;
- 判断记录是否已存在;
- 对重复记录执行更新;
- 同时维护主键索引和其他辅助索引。
这些过程都可能获取排他锁。
2.4 事务范围过大是重要放大因素
以下操作会增加死锁概率:
- 一个事务处理过多记录;
- 一个事务中混合执行多种写操作;
- 事务中包含远程调用、文件操作或其他耗时逻辑;
- 事务提交延迟,导致已获取的锁长时间不释放;
- 多个入口以不同顺序访问相同数据。
事务持锁时间越长,其他事务越容易在等待过程中形成反向依赖。
2.5 重试机制可能放大故障
死锁事务回滚后,如果消息系统或应用立即重试,并且重试仍然使用相同的数据顺序,可能出现:
死锁 -> 事务回滚 -> 立即重试 -> 再次竞争 -> 再次死锁因此,重试必须配合退避、随机等待和最大次数限制。
3. 定位过程
3.1 第一步:确认异常类型
区分以下几类异常:
| 异常 | 含义 |
|---|---|
| Deadlock / MySQL 1213 | 已经形成循环等待,数据库主动回滚事务 |
| Lock wait timeout / MySQL 1205 | 等待锁超时,不一定已经形成死锁 |
| Duplicate key | 唯一键冲突,不等同于死锁 |
| Connection timeout | 连接或网络问题,不等同于数据库锁问题 |
只有确认是死锁,才能按死锁路径定位和处理。
3.2 第二步:从异常 SQL 追踪完整事务
不能只分析报错的单条 SQL,需要继续向上追踪:
- 该 SQL 属于哪个事务;
- 事务此前执行过哪些 SQL;
- 事务一次处理了多少记录;
- 事务是否还执行了更新、删除或状态变更;
- 事务何时开始、何时提交;
- 是否存在异常捕获后继续使用当前事务的情况。
重点是还原“一个事务先拿了哪些锁,之后又申请了哪些锁”。
3.3 第三步:确认并发来源
检查所有可能写入同一数据资源的入口:
- Web 请求;
- 消息消费者;
- 定时任务;
- 异步线程;
- 批处理程序;
- 补偿任务;
- 运维脚本或人工 SQL。
单个进程或单个线程没有并发,不代表整个系统没有并发。还需要检查实例数量、消费者数量、重试任务和其他服务。
3.4 第四步:获取数据库死锁图
死锁发生后应立即执行:
SHOW ENGINE INNODB STATUS\G;重点查看:
- 死锁涉及的事务 ID;
- 每个事务最近执行的 SQL;
- 每个事务持有的锁;
- 每个事务等待的锁;
- 被回滚的事务;
- 锁对应的索引名称和记录范围。
如果数据库支持 Performance Schema,还应查看锁等待信息:
SELECT * FROM performance_schema.data_locks\G;SELECT * FROM performance_schema.data_lock_waits\G;3.5 第五步:确认表结构和索引
检查实际表结构,而不是只依据 ORM 或 Mapper 定义:
SHOW CREATE TABLE <table_name>\G;SHOW INDEX FROM <table_name>;重点确认:
- 是否存在多个唯一索引;
- 唯一索引的字段顺序;
- SQL 是否能够使用正确索引;
- 是否发生全表扫描;
- 是否存在较大的间隙锁范围;
- 主键和辅助索引是否都参与了锁竞争。
3.6 第六步:形成死锁证据链
一个完整的定位结论应包含:
事务 T1 执行了 SQL 顺序:A -> B事务 T2 执行了 SQL 顺序:B -> AT1 持有 A,等待 BT2 持有 B,等待 A数据库回滚 T1如果只能看到应用异常,不能看到另一个事务和锁等待关系,应将结论标记为“高概率原因”,不能表述为已经完全确认。
4. 问题修复方案
4.1 统一锁获取顺序
所有并发写入入口都应按照同一规则处理数据,例如按照唯一键的字段顺序排序:
先按字段 1 排序再按字段 2 排序最后按字段 3 排序只要所有事务访问相同数据时遵守一致顺序,就能显著降低锁顺序反转导致的死锁。
4.2 批内去重并控制批次大小
写入前应:
- 使用与数据库唯一键一致的规则去重;
- 按统一顺序排序;
- 控制单批记录数量;
- 避免同一事务处理过多数据。
需要注意:仅在一个外层事务中把大列表拆成多个 SQL,并不能完全释放前面 SQL 获取的锁。若要真正缩短锁持有时间,应让每个小批次独立提交。
4.3 使用原子写入
应优先使用数据库提供的原子插入或 Upsert,避免:
先查询是否存在再根据查询结果决定插入或更新查询和写入之间存在竞态窗口。多个事务可能同时判断“记录不存在”,然后同时尝试插入。
原子写入可以解决竞态和重复数据问题,但不能单独保证绝对没有死锁,仍需要统一顺序和事务重试。
4.4 缩短事务范围
建议将长流程拆分为:
事务 1:写入或锁定必要状态,立即提交事务外:执行远程调用或耗时操作事务 2:保存最终结果,立即提交不要在数据库事务中执行:
- 远程 HTTP/RPC 调用;
- 文件读写;
- 长时间等待;
- 大量计算;
- 不必要的查询。
4.5 优化索引
为查询和更新条件建立合理索引,使数据库尽量锁定精确记录,而不是扫描大范围索引区间。
索引优化的目标是:
- 减少扫描范围;
- 减少间隙锁;
- 缩短 SQL 执行时间;
- 缩短事务持锁时间。
索引不能替代事务设计,也不能替代死锁重试。
5. 代码层死锁防御
5.1 死锁发生后的正确处理流程
代码应遵循:
识别死锁异常 -> 当前事务完整回滚 -> 退避并随机等待 -> 重新开启事务 -> 重试完整操作5.2 必须重试整个事务
错误做法:
try { writeData();} catch (DeadlockException e) { writeData(); // 仍然在原事务中重试}正确做法是让原事务结束,再从事务入口重新执行:
外层重试逻辑 -> 新事务 -> 完整数据库操作 -> 成功提交死锁发生后,当前事务可能已经被数据库回滚,或者已经被事务管理器标记为只能回滚。此时不能继续复用当前事务。
5.3 重试策略
建议:
最大重试次数:2~3 次等待时间:指数退避等待时间:增加随机抖动超过次数:记录错误并进入补偿或死信流程只对确定的瞬时异常重试,例如:
- MySQL 1213;
- SQLState 40001;
- 数据访问层 Deadlock 异常。
业务参数错误、数据格式错误、权限错误和 SQL 语法错误不应无限重试。
5.4 消息确认策略
如果数据库写入由消息消费触发,应遵循:
事务成功提交 -> 确认消息消费成功事务死锁 -> 重试事务重试仍失败 -> 返回稍后重试或进入死信队列不能在事务失败后直接确认消息,否则可能造成数据丢失。
5.5 保证操作幂等
事务重试意味着同一请求可能执行多次,因此写入操作必须具备幂等性:
- 使用稳定的业务唯一键;
- 使用原子 Upsert;
- 避免重复生成不可恢复的副作用;
- 对外部操作使用幂等号或操作 ID;
- 将数据库提交结果作为最终成功标准。
5.6 增加日志和监控
每次任务批处理至少记录:
- 请求或消息 ID;
- 事务批次大小;
- 数据范围或唯一键范围;
- 重试次数;
- 异常码和 SQLState;
- 最终提交结果;
- 执行耗时。
建议监控:
deadlock_totaldeadlock_retry_totaldeadlock_final_failure_totaltransaction_latencylock_wait_latencybacklog_size并临时开启数据库死锁日志,保留完整死锁图,便于判断死锁参与者是否发生变化。
6. 复盘结论
本次故障应归因于并发事务之间的锁顺序反转,而不是某条业务数据本身异常:
并发事务+ 访问重叠数据+ 批量持有多条锁+ 锁获取顺序不一致+ 事务提交较晚=> 循环等待=> 数据库回滚一个事务最终防御方案为:
统一锁顺序+ 批内去重+ 原子写入+ 小批次独立事务+ 缩短事务范围+ 完整事务级重试+ 幂等和消息补偿本复盘文档只描述数据库死锁相关的通用原因、定位方法和代码防御原则,不绑定具体业务、服务、表名或实现类。具体修复效果仍需结合实际死锁图、表索引和并发压测验证。