数据库崩了别只会上香:死锁日志里藏着的4条“凶手线索”

发布时间:2026/5/21 19:32:39

数据库崩了别只会上香:死锁日志里藏着的4条“凶手线索” 大家好这里还是小耶。今天聊一个让我半夜爬起来好几次的话题数据库死锁。前阵子有个读者跟我描述了一个场景某个电商大促期间订单系统突然大面积报Deadlock found业务方快疯了。他赶紧执行了SHOW ENGINE INNODB STATUS看到LATEST DETECTED DEADLOCK下面一大段日志每个字母都认识串起来完全看不懂。CPU 一度飙到 70%所有 update/insert 都在排队等锁释放。死锁的可怕之处在于系统内有 4 万毫秒的强制锁释放超时设置这意味着单个锁的等待最多 40 秒。一旦上万个请求同时堆积不仅是数据库整个应用层在 40 秒内都会体验断崖式响应延迟。如果是核心交易系统用户看到的就是白屏和“网络异常”。死锁到底是什么先说明白这个问题。死锁是指两个或多个事务在执行过程中因争夺同一资源而互相等待结果谁也无法继续执行形成一个循环等待的僵局。事务 A 拿着资源 1 在等资源 2事务 B 拿着资源 2 在等资源 1谁也等不到最后只能被数据库内核强制中止其中一个。死锁通常发生在使用 InnoDB 行锁且并发较高的事务中。要让死锁发生必须同时满足四个条件​互斥​资源不能被共享一次只能被一个事务占用​持有并等待​事务已持有一部分资源同时还在等待其他事务持有的资源​非抢占​资源不能被强行剥夺必须由持有的事务主动释放​循环等待​事务之间形成了 A 等 B、B 等 A 的闭环理论上打破其中任何一个死锁就不会发生。但在高并发业务中这四者几乎会同时出现。InnoDB 在解决死锁时会做什么InnoDB 默认死锁检测是开启的。一旦检测到死锁会自动回滚其中一个事务通常选择执行成本较小或持有锁较少的事务来作为牺牲者另一个事务继续执行。这就是为什么Deadlock found错误往往是偶发性的——有时候死锁触发了其中一个事务被回滚报错出现有时候刚好绕过去了业务不受影响。但如果业务本身对事务提交失败没有重试机制每一次死锁都是一个定时炸弹。SHOW ENGINE INNODB STATUS 完整解读我第一次看到这个命令的输出满屏的十六进制地址、锁结构体完全看不懂。后来在线上一遍遍对日志、查文档才慢慢啃下来。今天把精华拆给你看。SHOW ENGINE INNODB STATUS返回的内容很长只会有一次最近的死锁记录如果发生过多次也只会保存最后一次所以运维上建议开启innodb_print_all_deadlocks ON把所有死锁日志记录进错误日志方便长期追踪。下面是一个典型死锁日志的核心结构拆解text------------------------ LATEST DETECTED DEADLOCK ------------------------ 2026-01-15 14:23:45 0x7f8a2c001700 *** (1) TRANSACTION: TRANSACTION 310298, ACTIVE 0 sec fetching rows mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 12345, OS thread handle 14000, query id 67890 10.0.0.1 app_user updating UPDATE orders SET status PAID WHERE order_id 10086 *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 100 page no 3 n bits 72 index PRIMARY of table db.orders trx id 310298 lock_mode X locks rec but not gap *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 100 page no 5 n bits 72 index idx_status of table db.orders trx id 310298 lock_mode X locks gap before rec insert intention waiting *** (2) TRANSACTION: TRANSACTION 310299, ACTIVE 0 sec fetching rows ... *** (2) HOLDS THE LOCK(S): ... *** (2) WAITING FOR THIS LOCK TO BE GRANTED: ... *** WE ROLL BACK TRANSACTION (1)逐行解读​**TRANSACTION​行**​ACTIVE 0 sec表示当前事务已经活跃 0 秒说明死锁在短时间内就被检测到了。​**mysql tables in use 1, locked 1**​事务用了几张表锁了几张表。​**LOCK WAIT**​当前事务正在等待锁。​**HOLDS THE LOCK(S)**​关键部分。表示当前事务已经拿到了哪些锁。这里lock_mode X表示排他锁locks rec but not gap表示行锁记录锁没有锁住间隙。看到gap before rec或insert intention时需要警惕间隙锁——很多死锁都源于间隙锁与插入意向锁的冲突。尤其是在 MySQL 默认的REPEATABLE READ隔离级别下间隙锁范围会被扩大死锁概率大增。​**WE ROLL BACK TRANSACTION (1)**​谁被选中作为牺牲者回滚了。可以据此调整事务顺序让不关键的 SQL 成为被回滚的一方。补充工具Performance Schema从 MySQL 8.0 开始官方推荐使用 Performance Schema 来更精细地监控锁竞争sql-- 查看当前所有锁信息 SELECT * FROM performance_schema.data_locks; -- 查看当前锁等待关系 SELECT * FROM performance_schema.data_lock_waits; -- 结合当前事务状态一起分析 SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.INNODB_TRX WHERE trx_state LOCK WAIT;这些视图能看到死锁发生前实际的阻塞链条比SHOW ENGINE INNODB STATUS的被动后置分析更主动。常见死锁场景与解决方案1. 多表访问顺序不一致最常见事务 AUPDATEt1→ UPDATEt2事务 BUPDATEt2→ UPDATEt1。解法统一规定业务代码中的访问顺序按表名或主键 ID 升序访问。2. 批量更新时的死锁SQL 执行计划异常导致全表扫描一次性锁了大量行。解法在 WHERE 条件上建立合适的索引缩小扫描范围。3. 并发插入时的间隙锁冲突RR 隔离级别下InnoDB 会锁住间隙防止幻读多个事务同时插入时可能互相等待。解法考虑降级为READ COMMITTED隔离级别RC 模式无间隙锁同时 binlog 格式设置为 ROW。4. 外键约束引发的死锁更新主表时不仅要锁本表还要检查关联子表高并发下容易死锁。一点个人体会我是运营转行做的 DBA。早期处理故障时总习惯先看监控大盘、重启应用、发公告——这是运营的“止血思维”。后来被线上问题教育了几次才明白DBA 得反过来先看锁日志、定位具体 SQL、再决定怎么处理。盲目重启只是掩盖问题过不了多久还会再崩。这个思维转换比学会任何一条命令都重要。死锁预防的最佳实践清单​统一加锁顺序​所有事务严格按相同顺序访问表和行。​拆分长事务​事务越短越好避免在事务中调用外部 API 或做耗时操作。​建好索引​让 WHERE 条件筛选尽可能少的行降低锁冲突范围。​考虑降隔离级别​业务允许时使用READ COMMITTED消除间隙锁导致的死锁。​添加重试机制​应用层捕获Deadlock found后重试往往能绕过瞬时的锁竞争。总结死锁排查的核心方法先用SHOW ENGINE INNODB STATUS确认死锁及涉及 SQL然后分析锁等待链找出形成循环的两个事务接着排查索引是否生效、是否存在全表扫描最后从代码层面统一访问顺序、拆分长事务、完善重试。死锁不是玄学它是数据库并发控制的必然产物。理解它、排查它是 DBA 和普通开发的分水岭。小耶在手SQL 不愁还有什么想了解的欢迎留言小耶一定知无不言言无不尽……我们下次见~参考文献MySQL官方文档《InnoDB Deadlocks》MySQL官方文档《SHOW ENGINE INNODB STATUS Reference》

相关新闻