死锁本质是事务加锁顺序冲突,而非并发高所致;需用EXPLAIN验证ORDER BY是否真走索引、避免无索引SELECT FOR UPDATE间隙锁、改用游标式批量更新并配合幂等重试。

死锁不是并发高导致的,而是多个事务以不同顺序锁定同一组行时立刻触发的——哪怕只有两个事务、每秒只跑几条 UPDATE,只要加锁顺序不一致,就可能秒级死锁。
UPDATE 死锁的本质是加锁顺序冲突
MySQL InnoDB 对 UPDATE 加锁不是“原子动作”,而是逐行扫描 + 条件匹配 + 行锁升级的过程。当两个事务对同一组记录(比如 id=1 和 id=5)以相反顺序加锁时,循环等待即刻形成:
- 事务 A 执行
UPDATE t SET x=1 WHERE id IN (1,5)→ 按索引顺序先锁 id=1,再锁 id=5 - 事务 B 同时执行
UPDATE t SET x=2 WHERE id IN (5,1)→ 若优化器走不同执行路径,可能先锁 id=5,再锁 id=1 - 结果:A 持有 id=1 等 id=5,B 持有 id=5 等 id=1 → InnoDB 检测到死锁,回滚其中一个(报错
Deadlock found when trying to get lock)
ORDER BY 不能直接防死锁,必须验证是否真生效
很多人以为加个 ORDER BY id 就安全了,但实际是否起作用,完全取决于执行计划是否真的用索引完成排序:
- 如果
WHERE status = 'pending'字段没索引,即使写了ORDER BY id,MySQL 也可能全表扫描后再 filesort → 锁住所有匹配行,且加锁顺序不可控 - 复合索引要注意最左前缀:
(user_id, created_at)索引下,WHERE user_id = 123 ORDER BY created_at能走索引;但WHERE status = 'x' ORDER BY created_at就无效 - 验证方法:对语句跑
EXPLAIN FORMAT=TRADITIONAL,确认key显示命中索引、Extra不含Using filesort
SELECT FOR UPDATE 在无记录时也会死锁
这是最容易被忽略的陷阱:RR 隔离级别下,SELECT ... FOR UPDATE 即使查不到记录,也会加间隙锁(Gap Lock),多个并发请求会争抢同一间隙,再叠加后续 INSERT 的插入意向锁,直接构成死锁链:
- 错误写法:
SELECT * FROM orders WHERE order_no = 'ABC' FOR UPDATE→ 记录不存在 → 所有并发请求都卡在同一个间隙上 - 正确替代:
INSERT INTO orders (order_no, user_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE updated_at = NOW(),前提是order_no有UNIQUE约束 - 若必须用 SELECT FOR UPDATE,确保 WHERE 条件字段有唯一索引,且
EXPLAIN显示type = const或ref、rows = 1
批量 UPDATE 的 LIMIT 边界问题会隐性放大死锁风险
LIMIT 在 UPDATE 中不保证幂等分页,尤其在高并发写入场景下,会导致同一批数据被重复处理或跳过,间接拉长事务时间、扩大锁持有窗口:
- 语句
UPDATE t SET status='done' WHERE status='pending' ORDER BY id LIMIT 100第一次取 id 1–100,第二次执行时若 id=50 已被其他事务更新,则这批数据“消失”,下次可能取到 id=101–200 —— 表面无错,实则漏处理 - 更稳做法是游标式推进:
UPDATE t SET status='done' WHERE id > 100 AND status='pending' ORDER BY id LIMIT 100,每次记录上一批最大id - 同时配合应用层幂等重试:捕获错误码
1213(MySQL),仅重试该 UPDATE 语句本身,最多 2~3 次,避免整个业务流程重放
真正难的不是写出“看起来安全”的 SQL,而是在每次加索引、改 WHERE 条件、调事务边界时,都去跑一遍 EXPLAIN,并确认死锁日志里不再出现那条语句——确定性,只能靠验证堆出来。











