MySQL死锁是事务间循环等待锁时InnoDB主动回滚一个事务的正常保护机制,非系统崩溃;通过SHOW ENGINE INNODB STATUS可查看最近一次死锁详情,包括事务ID、持有锁、等待锁及被回滚事务。

MySQL死锁不是“卡了”,而是两个或多个事务**各自拿着对方要的锁、又不肯先放**,系统检测到循环等待后主动挑一个事务回滚(报错 ERROR 1213 (40001): Deadlock found when trying to get lock),这不是崩溃,是 InnoDB 的正常保护机制。
死锁怎么一眼看出来?查 SHOW ENGINE INNODB STATUS 最直接
它会输出最近一次死锁的完整现场,关键信息包括:
-
*** (1) TRANSACTION和*** (2) TRANSACTION:两个冲突事务的 ID、状态、SQL 语句 -
HOLDS THE LOCK(S):谁持有什么锁(比如lock_mode X locks rec but not gap表示行排他锁) -
WAITING FOR THIS LOCK TO BE GRANTED:谁在等哪一把锁,等的是谁持有的 - 最后还会标出「
WE ROLL BACK TRANSACTION (1)」——说明哪个被牺牲了
注意:SHOW ENGINE INNODB STATUS 只保留最后一次死锁,高频死锁需配合错误日志(log_error_verbosity = 3)和监控采集。
为什么加锁顺序不一致就必死锁?InnoDB 的锁兼容性是硬约束
InnoDB 行锁本质是基于索引记录的,而锁是否冲突,取决于「锁类型 + 锁对象 + 事务隔离级别」。最典型场景:
-- 事务 A(按 id 升序加锁) START TRANSACTION; SELECT * FROM orders WHERE id IN (10, 20) FOR UPDATE;-- 事务 B(按 id 降序加锁,实际执行时 MySQL 仍会排序,但若逻辑上分步加锁就危险) START TRANSACTION; SELECT FROM orders WHERE id = 20 FOR UPDATE; SELECT FROM orders WHERE id = 10 FOR UPDATE;
这时 A 持有 10 → 等 20,B 持有 20 → 等 10,循环等待成立。根本原因不是“没排序”,而是**不同事务对同一组资源请求锁的路径不收敛**。
真正安全的做法是:所有业务统一按主键/索引字段升序(或固定规则)一次性锁定全部目标行,例如始终用 WHERE id IN (10, 20) FOR UPDATE,让 MySQL 内部按索引顺序加锁,避免人为制造交叉。
innodb_lock_wait_timeout 不是防死锁的,别配错
这个参数控制的是「锁等待超时」,单位秒,默认 50。它解决的是**长时间锁等待**(比如某事务卡住不提交),不是死锁。死锁检测是毫秒级的、完全独立的机制,由 InnoDB 自动触发,不受该参数影响。
常见误操作:
- 把
innodb_lock_wait_timeout设成 1 秒,以为能“快速失败避开死锁”——错,死锁仍会发生,只是你看到的错误从Deadlock found变成了Lock wait timeout exceeded - 设得过大(如 300 秒),导致阻塞堆积,掩盖真实瓶颈
建议值:线上业务保持默认 50;高并发短事务可调至 10~30,但必须配合应用层重试逻辑(尤其是捕获 ERROR 1213 后重试)。
间隙锁(Gap Lock)和 Next-Key Lock 是隐藏杀手
在 RR 隔离级别下,InnoDB 对范围查询(如 WHERE age BETWEEN 20 AND 30)会加 next-key lock(行锁 + 前面间隙锁)。这意味着:
- 即使两事务更新不同行,只要它们扫描的索引范围有重叠,就可能互相锁住彼此无法插入的“间隙”
- 唯一索引上的等值查询(
WHERE id = 100)只加行锁,不加间隙锁;但非唯一索引或范围查询,极易触发间隙锁竞争 - 死锁日志里出现
lock_mode X locks gap before rec就是典型间隙锁冲突
排查时重点看 WHERE 条件是否命中了非唯一索引、是否用了范围、是否缺少覆盖索引——这些都会扩大锁范围,增加死锁概率。
死锁不是配置调参能根治的问题,它是并发逻辑与数据访问模式的映射结果。最容易被忽略的一点:**应用层事务边界是否合理?一个本该 200ms 完成的库存扣减,如果裹着 HTTP 调用、远程日志、复杂校验,锁持有时间拉长到 2 秒,那再好的 SQL 也扛不住并发压测。**










