MySQL死锁是并发事务以不同顺序锁住相同资源导致的循环等待,InnoDB自动检测并回滚代价小的事务,报错“Deadlock found when trying to get lock”,本质是逻辑问题而非配置或性能问题。

Deadlock found when trying to get lock 在 MySQL 5.7 中不是配置错了,也不是服务器扛不住了,而是两个(或多个)事务以不同顺序锁住了同一组资源,且互相等待对方释放——InnoDB 检测到循环等待后,主动挑一个事务回滚,抛出这个错误。
它本质是并发逻辑问题,不是异常,更不是“要扩容”或“调大超时时间”就能绕开的。
为什么 UPDATE 语句在并发下容易触发死锁
MySQL 5.7 默认隔离级别是 REPEATABLE READ,InnoDB 对 UPDATE 加的是 next-key lock(行锁 + 间隙锁),尤其在范围条件、缺失索引、唯一键冲突场景下,锁的范围远超你肉眼看到的那几行。
常见诱因包括:
-
WHERE条件没走索引 → 全表扫描 → 锁住大量无关行 → 增加交叉概率 - 并发执行
INSERT ... ON DUPLICATE KEY UPDATE,且涉及相同唯一键(如songid_idx),但插入顺序不一致(A 先插 16 再插 17,B 先插 17 再插 16) - 多表更新(如订单+库存)未统一顺序:事务 A 先
UPDATE order后UPDATE stock,事务 B 反过来 - 使用动态 SQL(如 MyBatis 的
<if>拼条件),导致相同业务逻辑生成不同 WHERE 顺序,加锁路径分裂
注意:UPDATE 不是“只锁目标行”,它先按索引定位,再对搜索路径上的所有匹配/可能匹配位置加锁——间隙锁就是在这儿悄悄埋雷的。
怎么从 SHOW ENGINE INNODB STATUS 看懂死锁现场
执行 SHOW ENGINE INNODB STATUS\G 后,直接跳到 LATEST DETECTED DEADLOCK 区域。别扫全文,盯三块:
-
TRANSACTION下的mysql thread id→ 关联你应用日志里的请求 trace ID -
HOLDS THE LOCK(S)→ 这个事务当前锁住了哪些索引、哪几行(注意rec but not gap是纯行锁,gap before rec是间隙锁) -
WAITING FOR THIS LOCK TO BE GRANTED→ 它卡在哪条 SQL、哪个索引上,和另一个事务的HOLDS是否形成交叉
关键动作:
- 对照表结构,确认
WHERE字段是否有索引;没有?立刻补 - 如果
WAITING是price BETWEEN 100 AND 500,而price是普通索引(非唯一),那大概率是间隙锁打架 - 若
HOLDS出现PRIMARY锁多行,但 SQL 只查单 ID → 检查是否用了LIKE '%xxx'或函数导致索引失效
为什么统一主键升序能快速降低死锁率
死锁高频发生在“多行更新顺序不一致”。例如两个事务都要更新 id IN (101, 203, 57),但一个按自然顺序,一个按传入顺序,InnoDB 就会按不同物理顺序加锁,极易形成 A→B→C 和 C→A→B 的环。
解决方法简单粗暴:
- 所有批量
UPDATE或SELECT ... FOR UPDATE,强制加ORDER BY id ASC - 应用层收到 ID 列表后,先
sort()再拼 SQL(哪怕 ORM 不支持,也手动处理) - 对于
INSERT ... ON DUPLICATE KEY UPDATE场景,确保输入 ID 列表始终有序,避免靠数据库排序(InnoDB 不保证插入顺序加锁)
这不是“最佳实践建议”,而是 MySQL 5.7 在 RR 隔离级别下的实际加锁行为决定的硬约束:锁顺序 = SQL 执行时的索引遍历顺序。
间隙锁(Gap Lock)是怎么在背后搞事情的
在 REPEATABLE READ 下,WHERE status = 'pending' AND created_at > '2026-06-01' 这类查询,即使 status 有索引,只要 created_at 没覆盖索引,InnoDB 就可能对 created_at 范围做间隙锁定——两个事务同时执行,就可能互锁“下一个 pending 记录该插在哪”的空隙。
验证方式:
- 查看执行计划:
EXPLAIN输出中key_len明显小于联合索引总长度 → 说明没用全索引 -
SHOW CREATE TABLE确认是否建了(status, created_at)覆盖索引 - 临时降级隔离级别测试:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,如果死锁消失,基本可断定是间隙锁引发
注意:READ COMMITTED 会禁用间隙锁,但代价是可能读到幻读——得业务侧评估容忍度,不能无脑切。
真正难处理的从来不是报错本身,而是你以为改了 SQL 就完事,结果发现 ORM 自动生成的 WHERE 顺序、连接池里残留的旧事务、甚至从库并行复制的 writeset 分组逻辑,都在偷偷改变锁的实际获取路径。


















