死锁排查进阶:从日志到根因的完整分析链

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

之前讲过死锁的基本排查,今天我们来进阶。先问一个问题:你遇到死锁后,是不是只看了最后一次死锁日志,改了一下SQL,然后祈祷不要再出现?如果答案是"是",那这篇文章就是为你准备的。死锁不是玄学,它有固定的模式,也有系统化的分析方法和预防策略。

死锁怎么理解呢?

好比,十字路口,两辆车(事务A和事务B)互相挡住了对方的路(A锁了资源1等资源2,B锁了资源2等资源1),谁都动不了,只能交警(InnoDB)强制拖走一辆车(回滚其中一个事务)。但我们不能每次都靠交警拖车,而是要理解路口的设计,避免堵塞发生。


一、死锁的四种常见类型

根据锁冲突的对象和范围,死锁可以分为以下四种典型模式:

类型 触发条件 典型日志特征
1. 不同表死锁 事务A更新表1→表2,事务B更新表2→表1 两个HOLDS在不同的表上
2. 相同表不同行死锁 事务A更新行1→行2,事务B更新行2→行1(顺序相反) 同一索引的不同记录锁
3. 间隙锁死锁 事务A范围查询加间隙锁,事务B插入该间隙 lock_mode X locks gap before rec
4. 外键死锁 主表更新触发子表外键检查,与另一事务的子表操作冲突 涉及父表和子表的锁

下面逐一解释。

类型1:不同表死锁(最常见)

事务A:UPDATE t1 ... ; UPDATE t2 ...

事务B:UPDATE t2 ... ; UPDATE t1 ...

日志特征

复制代码
*** (1) HOLDS THE LOCK(S): index PRIMARY of table `db`.`t1`
*** (1) WAITING FOR THIS LOCK: index PRIMARY of table `db`.`t2`
*** (2) HOLDS THE LOCK(S): index PRIMARY of table `db`.`t2`
*** (2) WAITING FOR THIS LOCK: index PRIMARY of table `db`.`t1`

解决方案:统一所有事务中多表更新的顺序(如按表名字典序)。

类型2:相同表不同行死锁

事务A:UPDATE ... WHERE id=1; UPDATE ... WHERE id=2

事务B:UPDATE ... WHERE id=2; UPDATE ... WHERE id=1

日志特征 :两个事务的HOLDSWAITING在同一张表的不同主键记录上。

解决方案 :统一更新行的顺序(如按主键升序),或使用SELECT ... FOR UPDATE提前锁定所有行。

类型3:间隙锁死锁(RR隔离级别下)

事务A:SELECT * FROM t WHERE id > 100 FOR UPDATE(加间隙锁,防止幻读)

事务B:INSERT INTO t VALUES (150, ...)(等待间隙锁)

事务A再插入时可能死锁。

日志特征lock_mode X locks gap before recinsert intention 同时出现。

解决方案 :将隔离级别降为READ COMMITTED(消除间隙锁),或优化查询避免大范围锁定。

类型4:外键死锁

事务A:删除主表记录(触发子表外键检查,加锁子表)

事务B:插入子表记录(需要主表对应记录的锁)

日志特征:两个事务分别持有父表和子表的锁。

解决方案:减少外键约束,在应用层维护引用完整性;或调整事务顺序。


二、从死锁日志到根因的完整分析链

当遇到死锁时,不要只看最后几行。以下是一个系统化的分析流程:

步骤1:捕获完整日志

确认innodb_print_all_deadlocks = ON,将所有死锁记录到错误日志,而不是只看SHOW ENGINE INNODB STATUS的最后一次。

步骤2:定位死锁涉及的事务

日志中会有*** (1) TRANSACTION*** (2) TRANSACTION两个事务块。记录下:

  • 每个事务正在执行的SQL

  • 每个事务已经持有的锁(HOLDS THE LOCK(S)

  • 每个事务正在等待的锁(WAITING FOR THIS LOCK

步骤3:提取锁信息

注意lock_mode X(排他锁)、locks rec but not gap(行锁)、locks gap before rec(间隙锁)、insert intention(插入意向锁)。这些决定了死锁类型。

步骤4:判断死锁类型

根据上面的四种类型匹配,确定是不同表、相同表不同行、间隙锁还是外键导致的。

步骤5:分析业务逻辑

为什么这两个事务会在同一时间以这种顺序访问资源?通常是因为:

  • 应用代码中事务边界不合理(过长事务)

  • 批量操作与单条操作并发

  • 不同接口的访问顺序不一致

步骤6:制定解决方案

根据类型选择对应的解决策略(见第四部分)。


三、工具辅助:performance_schema深入排查

MySQL 5.7+提供了更细粒度的锁监控:

复制代码
-- 查看当前所有锁
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';

这些视图可以帮助你在死锁发生前就识别出潜在的锁竞争,提前优化。


四、预防死锁的最佳实践清单

策略 说明 效果
统一访问顺序 跨表操作按固定顺序(如表名、主键升序) 根除不同表/不同行死锁
缩短事务 事务中不要调用外部API或长时间计算 降低锁持有时间
使用RC隔离级别 若业务允许,用READ COMMITTED替代REPEATABLE READ 消除间隙锁死锁
添加合理索引 让UPDATE/DELETE走索引,避免行锁升级为表锁 减少锁范围
应用层重试 捕获死锁异常后自动重试 提高业务容错
监控死锁频率 设置告警,死锁超过阈值主动介入 及时发现问题

五、价值总结

死锁排查不是"猜谜",而是一套系统化的分析工程。从捕获日志、识别类型、分析根因到制定预防措施,每一步都有章可循。掌握了这套方法,你就能从"每次遇到死锁都手忙脚乱"变成"有条不紊地定位问题、提前预防"。死锁不可避免,但频繁死锁完全可以避免。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~

参考文献

  1. MySQL官方文档:《InnoDB Deadlocks》

  2. 《高性能MySQL》第4版,第7章:锁与事务

相关推荐
三无推导1 小时前
无需扩展的 PHP 加密方案有哪些优势:基于 php.x5.chat 的实践分析
开发语言·php·web开发·数据加密·php加密·php安全·无需扩展
jingling5551 小时前
Flutter | 商城项目鸿蒙(OpenHarmony)适配实战
android·开发语言·前端·flutter·华为·harmonyos
Luminous.1 小时前
C语言--day25
c语言·开发语言
A-刘晨阳1 小时前
数据库挂了服务就瘫?我用PostgreSQL主从流复制搭了高可用架构,cpolar打通远程访问
数据库·postgresql·架构
一个儒雅随和的男子1 小时前
Modbus通信协议原理
数据库
QT-Neal1 小时前
C++智能指针使用详解
开发语言·c++
JAVA9651 小时前
JAVA面试-并发篇 06-ReentrantLock如何实现公平锁的以及可重入吗
java·开发语言·面试
码农阿豪1 小时前
Node.js 连接金仓数据库踩坑记(上篇):环境搭建与基础操作
数据库·node.js
二等饼干~za8986682 小时前
geo优化系统源码搭建保姆式搭建教程
java·开发语言·django·php·音视频