死锁主因是事务顺序不一致、索引缺失和间隙锁滥用;修复需确保WHERE条件命中唯一索引、统一多表操作顺序,并用EXPLAIN验证执行计划。

核心结论:死锁无法彻底避免,但90%以上的高并发写入死锁,源于事务顺序不一致 + 索引缺失 + 间隙锁滥用。优先修复这两点,比调参数、加重试更治本。
为什么 SELECT ... FOR UPDATE 会触发死锁?
很多人以为它只是“查一行再锁一行”,实际在 InnoDB 中,它的行为取决于 WHERE 条件是否命中唯一索引:
- ✅
SELECT * FROM orders WHERE order_no = 'ORD-123' FOR UPDATE:命中唯一索引 → 只加 记录锁(Record Lock),不锁间隙,冲突极小 - ⚠️
SELECT * FROM orders WHERE user_id = 1001 FOR UPDATE:user_id无索引或非唯一 → 全表扫描 + 每行都加锁 → 实际退化为 表级竞争,极易死锁 - ❌
SELECT * FROM orders WHERE status = 'PENDING' AND create_time > '2026-06-01' FOR UPDATE:范围查询 → 触发 Next-Key Lock(记录锁 + 间隙锁),锁住整个符合条件的索引区间,多个事务范围重叠就互相阻塞
如何让 INSERT ... ON DUPLICATE KEY UPDATE 不卡住?
这个语句看似原子,但 InnoDB 内部仍分三步:查找 → 加锁 → 插入或更新。问题出在“查找阶段”:
- 如果
ON DUPLICATE KEY依赖的字段(如order_no)没有 唯一索引,InnoDB 无法快速定位,会扫描并尝试锁大量无关行 - 即使最终是插入,查找过程也会对“可能插入的位置”加 间隙锁,导致其他事务在相同间隙插入时被阻塞
- 解决方案不是换语法,而是确保:
ALTER TABLE orders ADD UNIQUE INDEX idx_order_no (order_no)—— 唯一索引能让查找变成 O(1),间隙锁收缩为零
事务顺序不一致是死锁最常见原因
两个事务更新同一组表,但顺序相反,是死锁的黄金模板:
事务A: UPDATE accounts SET balance = balance - 100 WHERE user_id = 123; UPDATE orders SET status = 'PAID' WHERE user_id = 123; <p>事务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)串行化热点数据路径,别指望数据库扛住
容易被忽略的关键点
很多团队花时间调 innodb_lock_wait_timeout 或加重试,却漏掉更底层的问题:
-
EXPLAIN必须成为每次上线前的检查项:对所有带FOR UPDATE、UPDATE、DELETE的语句跑一遍,确认type是const、ref或range,绝不能是ALL - 复合索引要满足最左前缀,比如常用
WHERE status = ? AND user_id = ?,索引必须建为(status, user_id),反过来无效 - 事务里禁止出现
SLEEP()、远程 HTTP 调用、大对象序列化等耗时操作——锁持有时间每多 100ms,死锁概率指数上升


















