线上业务突然报错,日志里冒出一句 Deadlock found when trying to get lock——这就是数据库死锁。两个事务各自握着对方要的锁,谁也等不到对方放手,MySQL 只好挑一个回滚掉。死锁要是频繁起来,轻则接口超时,重则业务直接断掉。下面从排查到根治,一步步说清楚。
1. 第一步:确认死锁发生了
最直接的证据就是应用层报错。MySQL 的 InnoDB 引擎一旦检测到死锁,会自动回滚其中一个事务,并抛出 1213 - Deadlock found when trying to get lock; try restarting transaction。只要在错误日志或异常堆栈里看到这条,基本就能确认是死锁。
另外可以翻一下 InnoDB 的状态信息:
SHOW ENGINE INNODB STATUS;
输出里的 LATEST DETECTED DEADLOCK 段会记下一次死锁的完整现场,包括涉及的事务、被锁住的行、等待的锁类型等等。
2. 分析死锁日志
拿到 InnoDB Status 后,重点看这几项:
- TRANSACTIONS:列出卷进死锁的两个事务(TRX 1 和 TRX 2)。
- LOCK WAIT:每个事务在等哪把锁、锁的类型(S 锁 / X 锁)。
- HOLDS THE LOCK(S):每个事务当前握着哪些锁。
- SQL STATEMENT:触发死锁的那条 SQL。
把两个事务的「握着什么」和「等着什么」一排,就能看清那个环:事务 A 握着行 X、等行 Y;事务 B 握着行 Y、等行 X——典型的互相等待。
3. 常见原因与快速修复
原因一:加锁顺序不一致
这最容易引发死锁。要是业务代码里有时先改 A 表再改 B 表,有时反过来,交叉等待就出现了。修复办法:把所有操作对同一组表的加锁顺序统一,全局保持一致。
原因二:索引缺失导致全表扫描上锁
WHERE 条件没命中索引时,InnoDB 可能要给大量行甚至整张表加锁,冲突概率一下子就上去了。修复办法:给高频查询条件补上合适的索引,把锁的范围压下去。
原因三:大事务持有锁时间过长
一个事务里塞了太多事(多次查询 + 更新 + 外部调用),锁就被长时间占着。修复办法:把大事务拆小,减少单个事务里的操作量;别在事务里做网络调用或耗时计算。
原因四:外键级联操作触发隐式锁
外键约束里的 CASCADE / SET NULL 会隐式去拿子表的锁,可能跟别的事务撞上。修复办法:先想清楚到底需不需要这个外键,很多高并发场景会选择在应用层自己保证一致性。
4. 预防措施(长期方案)
- 开慢查询日志 + 死锁监控:把
innodb_print_all_deadlocks = ON设上,所有死锁都会写进错误日志,后面分析趋势方便得多。 - 合理设置锁等待超时:
innodb_lock_wait_timeout默认 50 秒,可以按业务调短一些(比如 5–10 秒),让等待方更快失败重试。 - 用乐观锁替代悲观锁:读多写少的场景,可以试试用版本号(version 字段)做乐观并发控制,从根上绕开锁竞争。
- 定期审查 SQL 与索引:死锁往往只是 SQL 写法或索引设计问题的表象,定期做 SQL 审计、跑跑 EXPLAIN,能把发生概率明显压下来。
5. 应急处理流程总结
- 收到告警或报错 → 先确认是不是 Deadlock 异常。
- 执行
SHOW ENGINE INNODB STATUS拿死锁详情。 - 定位涉及的表和 SQL → 分析加锁顺序和索引情况。
- 临时缓解:重启受影响的事务,或者短暂限流。
- 根治:改代码统一加锁顺序、补索引、拆分大事务。
死锁并不是什么搞不定的难题。排查思路和预防手段理顺之后,就能从到处救火变成提前防住。如果这篇文章帮你解决了问题,欢迎到 fenij.com 留言聊聊你的经历。