死锁主因是事务加锁顺序不一致与间隙锁叠加引发循环等待;修复需统一加锁序、缩小锁范围、避免无索引条件的select for update。

直接结论:死锁不是锁本身的问题,而是多个事务以不同顺序加锁 + 间隙锁叠加导致的循环等待。修复核心是统一加锁顺序、缩小锁范围、避免在无索引条件上用 SELECT FOR UPDATE。
为什么 SELECT FOR UPDATE 在无记录时也会死锁?
当查询条件(如 WHERE uid = ?)命中唯一索引但记录不存在时,InnoDB 会加“间隙锁”(Gap Lock),锁定该值可能插入的位置;多个事务同时执行相同语句,都会尝试锁定同一间隙,而后续 INSERT 又需要在该间隙内加“插入意向锁”——两者冲突,且因事务彼此等待,形成死锁。
- 典型错误模式:
SELECT * FROM orders WHERE order_no = 'ABC' FOR UPDATE→ 记录不存在 → 多个并发请求都卡在间隙锁上 → 同时INSERT→ 死锁 - 根本原因不是用了
FOR UPDATE,而是“先锁后判再插”的逻辑,在无索引或唯一性未被约束时,锁范围不可控 - RR 隔离级别下默认启用间隙锁,
READ COMMITTED可禁用间隙锁,但会牺牲可重复读语义,慎切
如何让 SELECT FOR UPDATE 真正只锁一行(或不锁)?
锁行为完全取决于 WHERE 条件是否能走索引、以及索引类型。必须确保:
- 查询字段有**唯一索引**(如
UNIQUE KEY idx_order_no (order_no)),否则即使order_no逻辑唯一,InnoDB 也无法精确锁定单行/单间隙 - 避免在
SELECT FOR UPDATE中使用函数、隐式类型转换、OR、LIKE '%xxx'等导致索引失效的写法 - 用
EXPLAIN验证执行计划:type应为const或ref,key显示命中索引,rows= 1 - 如果业务允许,可用
INSERT ... ON DUPLICATE KEY UPDATE替代“查-判-插”流程,完全绕过SELECT FOR UPDATE
高并发“查不存在则插入”场景的可靠替代方案
硬靠 SELECT FOR UPDATE 实现幂等插入,在并发稍高时极易触发死锁。更健壮的做法是把冲突控制交给唯一约束本身:
- 确保目标字段(如
order_no)有UNIQUE约束 - 应用层先尝试
INSERT INTO orders (order_no, user_id) VALUES (?, ?) - 捕获 MySQL 错误码
1062(Duplicate entry),说明已存在,按需返回或重试 - 无需显式事务包裹整个流程;若需原子性,仅对 INSERT + 后续更新加事务即可
- 对比原方案:少了锁等待、少了死锁回滚、吞吐更高——代价是应用层多一次异常处理
排查死锁时最该盯住的三处输出
别只看报错信息,SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 块里藏着关键线索:
- 找
*** (1) TRANSACTION:和*** (2) TRANSACTION:—— 看两个事务各自执行了哪条SELECT FOR UPDATE或INSERT - 看
lock_mode X locks gap before rec insert intention waiting这类描述——明确指向间隙锁 + 插入意向锁冲突 - 核对
TABLE LOCK行中的index名:是不是你预期的唯一索引?如果不是,立刻检查索引定义和查询条件
真正容易被忽略的是:死锁日志里显示的 SQL 往往是“最后执行的语句”,但加锁起点可能在前面几行没打印出来的语句中。务必结合应用代码里的事务边界一起看,否则永远在修表象。











