应通过对比LATEST DETECTED DEADLOCK中两个事务的HOLDS THE LOCK(S)与WAITING FOR THIS LOCK TO BE GRANTED字段,反推加锁顺序:例如事务1持idx_name锁等PRIMARY锁,事务2持PRIMARY锁等idx_name锁,即构成循环等待闭环。

怎么看 LATEST DETECTED DEADLOCK 里的加锁顺序
MySQL 8.0 的 SHOW ENGINE INNODB STATUS\G 输出中,LATEST DETECTED DEADLOCK 区块不是时间线,而是按事务编号(如 (1)、(2))组织的快照。关键不是“谁先执行”,而是“谁持有什么锁、在等什么锁”——这两者叠加起来,才能反推出加锁顺序。
真正要盯住的是每段里明确标出的 HOLDS THE LOCK(S) 和 WAITING FOR THIS LOCK TO BE GRANTED。比如:
-
(1) HOLDS THE LOCK(S): RECORD LOCKS ... index idx_name ... lock_mode X→ 事务1已对idx_name上某记录加了X锁 -
(1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS ... index PRIMARY ... lock_mode X locks rec but not gap waiting→ 同一事务1又想对主键加X锁,但被卡住 -
(2) HOLDS THE LOCK(S): RECORD LOCKS ... index PRIMARY ... lock_mode X→ 事务2正拿着主键X锁不放 -
(2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS ... index idx_name ... lock_mode X→ 事务2也在等idx_name的X锁
这就构成闭环:事务1持 idx_name 等主键,事务2持主键等 idx_name。顺序问题就藏在这两组“持有→等待”的映射里。
怎么区分 record lock、gap lock 和 next-key lock
锁类型直接决定冲突范围,看日志末尾的 lock_mode 描述即可,不用猜:
-
lock_mode X locks rec but not gap:纯记录锁,只锁索引项本身(如主键值=123) -
lock_mode X locks gap before rec:间隙锁,锁的是前一条记录到当前记录之间的空隙(如 (100, 123)) -
lock_mode X locks rec but not gap waiting或lock_mode S waiting:等待中的锁,类型和它前面那行HOLDS的一致,只是还没拿到 - 没写
gap也没写next-key,默认是 next-key lock(record + gap),这是 RR 隔离级别下 UPDATE/DELETE 的常规行为
特别注意:如果看到 lock_mode S waiting 且前面 HOLDS 里有 gap,大概率是唯一键冲突引发的间隙共享锁等待(比如 INSERT 唯一键重复时,会先加 S gap lock 检查是否存在),这在死锁链里非常隐蔽。
为什么同一个事务既 HOLD 又 WAIT 同一类锁
这不是日志错误,而是 MySQL 8.0 死锁日志增强后的真实表现。典型场景是唯一索引冲突 + 间隙锁竞争:
- 事务A执行
INSERT INTO t (c1, c2) VALUES (10, 10),c2是唯一索引 → 先加S lock on c2=10 gap检查是否已存在 - 事务B已插入
c2=10并未提交 → 持有X lock on c2=10 rec - 事务A的 S gap lock 被 B 的 X rec lock 阻塞 → 日志里显示事务A
HOLDS S lock gap且WAITING for S lock gap - 此时事务B再尝试插入
c2=9(落在同一 gap 内)→ 需要插入意向锁,又被事务A的 S gap lock 阻塞
于是两个事务互相卡在 gap 层面,日志就会把事务A的“持有”和“等待”都标为同一类 gap 锁。这种现象在 UNIQUE KEY + REPEATABLE READ 下高频出现,不能当成配置或版本 bug 处理。
如何关联死锁 SQL 和真实业务语句
日志里的 starting index read 或 updating or deleting 后面的 SQL 往往是简化版,甚至不含 WHERE 条件。必须结合其他信息交叉验证:
- 用
trx_id去查information_schema.INNODB_TRX,看trx_query字段——这是事务当前正在执行的完整语句(可能为空,说明已执行完但未提交) - 用
waiting_pid或blocking_pid查performance_schema.threads,确认该连接的PROCESSLIST_ID和用户、数据库、命令类型 - 重点比对
LOCK_DATA值:比如日志里写lock_data: 123,就去查对应表的主键或索引列,确认是不是业务里常说的「订单ID=123」或「用户ID=123」 - 如果
LOCK_DATA是字符串(如'Eason'),直接搜代码里 WHERE 条件是否用了这个字面量
最容易被忽略的是:死锁往往不是由报错那条 SQL 单独引发的,而是它之前某条未提交的 SELECT … FOR UPDATE 或 UPDATE 已经持有了锁。所以必须顺着 trx_started 时间往前翻几秒的事务操作,而不是只盯死锁现场那一行。


















