死锁主因是where条件未走唯一索引+多表操作顺序不统一;需确保select for update命中唯一索引、统一事务中表更新顺序、补全唯一索引以收缩间隙锁范围。

这不是锁用多了,而是锁的范围、顺序和索引没对上——90% 的 Lock usage yields a deadlock 错误,根子在 WHERE 条件没走唯一索引 + 多表操作顺序不统一。
SELECT FOR UPDATE 没命中唯一索引就等于开盲锁
很多人以为 SELECT ... FOR UPDATE 只是“查一行再锁一行”,实际 InnoDB 完全按执行计划来加锁:
- ✅
SELECT * FROM orders WHERE order_no = 'ORD-123' FOR UPDATE:如果order_no是唯一索引 → 只加记录锁(Record Lock),冲突极小 - ⚠️
SELECT * FROM orders WHERE user_id = 1001 FOR UPDATE:若user_id无索引或非唯一 → 全表扫描 + 每行都加 X 锁 → 实际退化为表级竞争,极易死锁 - ❌
SELECT * FROM orders WHERE status = 'PENDING' AND create_time > '2026-06-01' FOR UPDATE:范围查询 → 触发 Next-Key Lock(记录锁 + 间隙锁),多个事务范围重叠就互相阻塞
验证方式:在语句前加 EXPLAIN,看 type 是否为 const 或 eq_ref;key 列是否显示你预期的索引。
INSERT ON DUPLICATE KEY UPDATE 卡住,往往因为缺失唯一索引
这个语句看似原子,但 InnoDB 内部仍分三步:查找 → 加锁 → 插入或更新。问题出在“查找阶段”:
- 如果
ON DUPLICATE KEY依赖的字段(如order_no)没有唯一索引,InnoDB 无法快速定位,会扫描并尝试锁大量无关行 - 即使最终是插入,查找过程也会对“可能插入的位置”加间隙锁,导致其他事务在相同间隙插入时被阻塞
- 解决不是换语法,而是补索引:
ALTER TABLE orders ADD UNIQUE INDEX idx_order_no (order_no)
补完后,查找从 O(n) 降为 O(1),间隙锁收缩为零,死锁概率断崖下降。
多表更新顺序不一致是死锁黄金模板
两个事务更新同一组表,但顺序相反,就是最典型的死锁触发器:
- 事务 A:
UPDATE accounts SET balance = balance - 100 WHERE user_id = 123→UPDATE orders SET status = 'PAID' WHERE user_id = 123 - 事务 B:
UPDATE orders SET status = 'PAID' WHERE user_id = 123→UPDATE accounts SET balance = balance - 100 WHERE user_id = 123
解决方式不是靠数据库自动回滚,而是在代码层强制统一资源访问顺序:
- 所有涉及
accounts和orders的事务,必须固定为先accounts后orders - 不要在事务中动态决定先查哪张表;把顺序固化进 DAO 层或 Service 接口契约里
- 如果业务逻辑天然无法统一(比如 A 流程从订单出发、B 流程从账户出发),那就用应用层分布式锁(Redis)做串行化
间隙锁(Gap Lock)在 RR 隔离级别下静默放大死锁风险
InnoDB 默认隔离级别 REPEATABLE READ 会启用间隙锁,它不锁数据,只锁“空隙”。这在幂等校验场景中尤其危险:
- 建表时
order_no是普通索引(非唯一),两个事务同时执行:SELECT * FROM t_order WHERE order_no = 1007 FOR UPDATE - 由于不是唯一索引,InnoDB 会加 Next-Key Lock,锁住
(1006, 1008)这个间隙 - 两事务互锁间隙,谁也插不进新记录,形成死锁
关键点:间隙锁不可见、不报错、不释放,直到事务结束。它不像行锁那样容易被 SHOW ENGINE INNODB STATUS 直接识别为“持有某行”,排查时容易忽略。











