死锁是事务间循环等待锁的明确状态,非卡顿或超时;innodb通过检测deadlock found报错并回滚undo小的事务来解决,需同时满足互斥、持有并等待、不可剥夺、循环等待四条件。

死锁不是“卡住”,而是两个事务互相拿着对方要的锁
MySQL(InnoDB)检测到死锁时,报错一定是 Deadlock found when trying to get lock,而不是超时或连接中断。这说明不是慢、不是堵,是明确的循环等待:事务 A 持有行 X 的排他锁(X lock),正等着行 Y;事务 B 恰好持有 Y 的 X 锁,又反过来等 X。双方都不肯先松手,InnoDB 只能选一个回滚——通常挑「undo log 更小」的那个事务,因为它回滚成本低。
关键点在于:死锁必须同时满足四个条件:互斥(X 锁不兼容)、持有并等待(已锁一行,还要锁另一行)、不可剥夺(锁不能被强抢)、循环等待(A→B→A)。InnoDB 默认 RR 隔离级别下,间隙锁(Gap Lock)和 next-key 锁会放大锁范围,让原本不相关的行也卷入等待链,这是线上死锁高发的底层推手。
最常见死锁场景:加锁顺序不一致 + 无索引扫描
比如两个并发事务更新同一张 user_order 表:
- 事务 A 执行:
UPDATE user_order SET status = 'paid' WHERE user_id = 1001 AND order_no = 'O20260128001' FOR UPDATE - 事务 B 同时执行:
UPDATE user_order SET amount = amount + 10 WHERE order_no = 'O20260128002' AND user_id = 1002 FOR UPDATE
如果 user_id 和 order_no 都没联合索引,InnoDB 可能全表扫描,按聚簇索引物理顺序逐行加锁。A 先锁了第 5 行,B 先锁了第 3 行;接着 A 要锁第 3 行,B 要锁第 5 行——死锁闭环就形成了。
更隐蔽的是 ORM 自动生成 SQL:GORM 可能把 WHERE 条件顺序随机拼成 user_id = ? AND order_no = ? 或反过来,导致不同服务实例加锁路径不一致。这不是 bug,是设计缺陷。
怎么快速定位谁在搞鬼?看 SHOW ENGINE INNODB STATUS 就够了
这个命令输出里,LATEST DETECTED DEADLOCK 区块才是救命信息。别只扫 SQL,重点盯三处:
-
事务持有的锁:看
*** (1) HOLDS THE LOCK(S):下面的RECORD LOCKS space id ...,确认它锁了哪些索引、哪几行(hex primary key值可转成实际 ID) -
事务申请的锁:看
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:,对比另一个事务的 HOLDING 部分,就能画出等待图 -
索引使用情况:检查每条 SQL 后面是否写着
index `idx_user_id`,如果显示index PRIMARY或index `GEN_CLUST_INDEX`,基本就是没走有效索引,正在全表扫描加锁
注意:MySQL 8.0+ 可用 performance_schema.data_locks 查实时锁,但生产环境别频繁查,它本身会加 metadata lock,可能雪上加霜。
真正有效的解决动作只有三个,别做无用功
改代码比调参数管用十倍。以下操作按优先级排序:
-
统一加锁顺序:所有涉及多行更新的逻辑,先
SELECT ... ORDER BY id ASC FOR UPDATE把要改的主键 ID 排好序,再按顺序批量更新。哪怕只是两行,也要强制排序 -
补唯一/覆盖索引:确保
WHERE条件中所有字段都在同一个索引里,且顺序匹配查询模式。例如高频查user_id = ? AND status = ?,就建INDEX idx_user_status (user_id, status),别只建单列索引 - 拆长事务:把 HTTP 请求里夹带的「查余额 → 扣库存 → 写日志 → 发消息」全塞进一个事务,等于主动邀请死锁。把非核心操作(如发 MQ、写审计日志)挪到事务外异步执行
别迷信 SET innodb_deadlock_detect=OFF——它只是关掉检测,让锁等待变成超时(innodb_lock_wait_timeout),问题还在,只是报错变慢而已。真正的复杂点永远在业务逻辑层:你无法靠数据库配置绕过「资源竞争本质」,只能靠设计收敛访问路径。










