MySQL 死锁排查:从事务边界到锁顺序治理
死锁是两个或多个事务形成循环等待。InnoDB 会检测并回滚其中一个事务,因此应用看到的通常是一条 Deadlock found when trying to get lock。正确处理不是关闭检测或无限重试,而是还原涉及的事务、锁对象和访问顺序。
第一时间保留证据
SHOW ENGINE INNODB STATUS\G
LATEST DETECTED DEADLOCK 会展示相关事务、正在执行的 SQL、已持有和正在等待的锁。这个区域只保留最近一次,监控系统应及时采集。MySQL 8 还可以通过 Performance Schema 查看当前锁等待:
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
只看报错 SQL 不够。事务可能在更早的语句已经持锁,因此还要从应用日志关联同一事务中的完整调用序列和参数。
最常见的形成方式
不一致的更新顺序
事务 A 先更新用户 1 再更新用户 2,事务 B 顺序相反,就可能互相等待。解决方式是所有路径按稳定主键顺序加锁:
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
for _, id := range ids {
// 按统一顺序执行 SELECT ... FOR UPDATE 或 UPDATE
}
缺少合适索引
更新条件没有索引时,数据库可能扫描并锁住远超预期的记录。先用 Explain 确认访问范围,再设计索引。索引越多不一定越好,不同索引路径也可能改变加锁顺序。
事务包含外部 I/O
开启事务后调用 HTTP、MQ 或等待用户输入,会显著延长持锁时间。事务里只保留必要数据库操作;外部通知使用 Outbox 等事务后机制。
批量事务过大
一次更新上万行会扩大锁集合,也更容易和其他业务交叉。按可恢复批次处理,并为每批设置明确上限。
应用层必须允许有限重试
即使设计良好,复杂并发系统仍可能偶发死锁。事务应具备幂等性,并只对明确的死锁或锁等待错误进行有限重试:
for attempt := 0; attempt < 3; attempt++ {
err := runTransaction(ctx)
if !isDeadlock(err) {
return err
}
time.Sleep(backoffWithJitter(attempt))
}
每次重试必须重新开始整个事务,不能在已失败事务上继续。退避需要随机抖动,避免竞争双方同步重试再次碰撞。超过次数后返回可观测错误,不要无限循环。
治理清单
- 缩短事务,禁止事务内远程调用。
- 多行操作按统一主键或业务键顺序执行。
- 为更新和锁定条件提供准确索引。
- 记录事务 ID、关键参数和完整 SQL 顺序。
- 对死锁做有限、幂等、带抖动的整体重试。
- 用并发测试复现,而不是只跑串行单测。
死锁不是单纯的数据库参数问题,它通常暴露了业务资源访问顺序不统一。修复后应把顺序规则写进仓储层公共方法,避免不同业务再次各写一套。

评论
0 条讨论