MySQL死锁排查实战:我如何用SHOW ENGINE INNODB STATUS揪出隐藏的资源竞争
MySQL死锁排查实战我如何用SHOW ENGINE INNODB STATUS揪出隐藏的资源竞争那个凌晨的报警电话凌晨2点我被手机震醒。打开钉钉一条刺眼的报警信息“订单系统数据库死锁大量请求超时”。我揉了揉眼睛登录服务器发现错误日志里满是这样的信息ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction说实话那一刻我是懵的。死锁什么死锁谁的死锁为什么会死锁我花了整整3个小时才把这事儿搞清楚。今天我就把这整个过程分享给你希望下次你遇到类似问题时不用像我一样手忙脚乱。先搞明白死锁到底是什么死锁这玩意儿说白了就是两个或多个事务互相卡住了。想象这样一个场景事务A拿着资源1想要资源2事务B拿着资源2想要资源1两个人都攥着自己手里的不放又都等着对方手里的。结果就是——谁都动不了。在MySQL里死锁通常长成这样-- 事务ABEGIN;UPDATEusersSETbalancebalance-100WHEREid1;-- 持有id1的锁UPDATEusersSETbalancebalance100WHEREid2;-- 等待id2的锁COMMIT;-- 事务BBEGIN;UPDATEusersSETbalancebalance-50WHEREid2;-- 持有id2的锁UPDATEusersSETbalancebalance50WHEREid1;-- 等待id1的锁COMMIT;什么场景最容易触发死锁并发高事务处理时间长批量操作一次性锁住大量数据事务里UPDATE/DELETE的顺序不一致间隙锁Gap Lock导致的锁范围重叠救命稻草SHOW ENGINE INNODB STATUS当我发现系统出现死锁时第一反应就是执行这条命令SHOWENGINEINNODBSTATUS;这条命令会输出InnoDB引擎的详细状态信息其中最关键的就是LATEST DETECTED DEADLOCK部分——它记录了最近发生的死锁详情。让我给你看看真实的输出长什么样------------------------ LATEST DETECTED DEADLOCK ------------------------ 2026-04-04 02:15:32 0x7f8b4c0a9700 *** (1) TRANSACTION: TRANSACTION 2834756, ACTIVE 0.001 sec starting index read 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 140236234237696, query id 67890 localhost 192.168.1.100 app_user updating UPDATE orders SET status PAID WHERE order_no ORD202604040001 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 456 n bits 72 index idx_order_no of table trade_db.orders trx id 2834756 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 2834755, ACTIVE 0.003 sec fetching rows mysql tables in use 1, locked 1 2 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 12346, OS thread handle 140236234236544, query id 67891 localhost 192.168.1.101 app_user updating UPDATE orders SET status SHIPPED WHERE order_no ORD202604040002 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 123 page no 456 n bits 72 index idx_order_no of table trade_db.orders trx id 2834755 lock_mode X locks rec but not gap *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 456 n bits 72 index idx_order_no of table trade_db.orders trx id 2834755 lock_mode X locks rec but not gap waiting *** WE ROLL BACK TRANSACTION (2)看到这么多信息是不是有点晕别急我来给你划重点。如何读懂死锁日志上面的日志看起来很复杂其实核心信息就几块1. 时间戳死锁发生的具体时间方便你去业务日志里定位问题。2. 事务1的信息受害者视角2834756事务IDACTIVE 0.001 sec事务活跃时间时间越长越危险正在执行的SQL语句3. 事务1在等待什么锁lock_mode X排他锁X锁写操作会加这种锁waiting正在等待4. 事务2的信息持有锁的罪魁祸首注意事务ID和事务1不同说明是两个独立的事务。5. 事务2持有什么锁这是最关键的信息——它告诉你是哪个事务在捣乱。6. 最后的裁决*** WE ROLL BACK TRANSACTION (2)MySQL选择回滚事务2牺牲它来解除死锁。事务1得以继续执行。实战案例一个可复现的死锁场景光说不练假把式我来给你搭一个能复现死锁的环境。1. 准备测试数据-- 创建测试表CREATETABLEIFNOTEXISTSaccounts(idINTPRIMARYKEYAUTO_INCREMENT,user_nameVARCHAR(50)NOTNULL,balanceDECIMAL(10,2)NOTNULLDEFAULT0.00,INDEXidx_balance(balance))ENGINEInnoDBDEFAULTCHARSETutf8mb4;-- 插入测试数据INSERTINTOaccounts(user_name,balance)VALUES(Alice,1000.00),(Bob,1000.00),(Charlie,1000.00);2. 复现死锁打开两个MySQL客户端窗口按以下顺序执行窗口1事务ABEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;-- 此时不要提交去窗口2执行窗口2事务BBEGIN;UPDATEaccountsSETbalancebalance-50WHEREid2;-- 此时不要提交回窗口1继续窗口1事务A继续UPDATEaccountsSETbalancebalance100WHEREid2;-- 这里会等待因为事务B持有id2的锁窗口2事务B继续UPDATEaccountsSETbalancebalance50WHEREid1;-- 这里会触发死锁MySQL会报错并回滚事务B事务B会收到错误ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction这时候你去执行SHOW ENGINE INNODB STATUS就能看到完整的死锁日志了。解决方案如何避免死锁死锁虽然不能完全避免但可以大大降低发生的概率。1. 统一操作顺序这是我踩坑后总结出来的第一条铁律所有事务对相同资源的访问顺序必须一致。-- 好的做法两个事务都按id升序访问事务A:UPDATE...WHEREid1;UPDATE...WHEREid2;事务B:UPDATE...WHEREid1;UPDATE...WHEREid2;-- 坏的做法顺序不一致事务A:UPDATE...WHEREid1;UPDATE...WHEREid2;事务B:UPDATE...WHEREid2;UPDATE...WHEREid1;-- 死锁2. 尽量缩短事务长度事务越长持有锁的时间越久死锁概率越高。-- 坏的做法事务里塞一堆业务逻辑BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;-- 这里调用外部API可能耗时几秒UPDATEaccountsSETbalancebalance100WHEREid2;COMMIT;-- 好的做法先拿到数据业务逻辑放外面-- 1. 查询数据-- 2. 业务处理-- 3. 开启事务快速完成更新BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;UPDATEaccountsSETbalancebalance100WHEREid2;COMMIT;3. 使用合适的隔离级别如果业务允许可以考虑把隔离级别从默认的REPEATABLE READ降到READ COMMITTED减少间隙锁的使用。SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;4. 添加监控和告警死锁发生后MySQL会自动处理但你需要知道它发生了。可以在应用层捕获ERROR 1213并上报或者在MySQL层开启死锁监控-- 查看死锁统计SHOWGLOBALSTATUSLIKEInnodb_deadlock%;-- 开启死锁检测日志MySQL 8.0SETGLOBALinnodb_print_all_deadlocksON;写在最后那次凌晨的死锁事件最后发现是我们新上线的一个功能导致的。那个功能里有两个并发任务分别更新了两张关联表但更新的顺序正好相反。我花了3个小时排查最后只改了一行代码——把两个任务的更新顺序统一了。但如果不理解死锁的原理我可能还在到处加索引、调参数。死锁不可怕可怕的是不知道怎么排查。希望这篇文章能帮到你。如果你也踩过类似的坑欢迎在评论区交流。PS最后分享一个我后来写的死锁监控脚本放到cron里每小时跑一次有死锁就发钉钉告警#!/bin/bash# deadlock_monitor.shDEADLOCK_COUNT$(mysql-eSHOW GLOBAL STATUS LIKE Innodb_deadlocks;|grep-o[0-9]*$)if[$DEADLOCK_COUNT-gt0];thencurl-XPOSThttps://oapi.dingtalk.com/robot/send?access_tokenxxx\-HContent-Type: application/json\-d{\msgtype\:\text\,\text\: {\content\:\⚠️ MySQL死锁告警过去一小时发生$DEADLOCK_COUNT次死锁\}}fi