死锁本质是事务加锁顺序冲突而非并发高所致;需统一加锁顺序、显式for update一次性锁定、缩短事务、确保索引命中并合理设置隔离级别。

死锁不是并发高导致的,而是多个事务以不同顺序锁定同一组行时立刻触发的——哪怕只有两个事务、每秒只跑几条 UPDATE,只要加锁顺序不一致,就可能秒级死锁。
UPDATE 的 ORDER BY 不等于加锁顺序安全
很多人加 ORDER BY id 就以为锁顺序可控,但 MySQL 实际是否按这个顺序加锁,完全取决于执行计划是否真走索引。如果 WHERE 条件没索引,优化器很可能全表扫描 + filesort,结果锁住所有匹配行,且加锁顺序不可控。
- 验证方法:对语句执行
EXPLAIN FORMAT=TRADITIONAL,确认key显示命中索引、Extra不含Using filesort - 复合索引要注意最左前缀:有
(status, id)索引时,WHERE status = 'pending' ORDER BY id才能生效;若只写WHERE id > 100,该索引基本无效 -
ORDER BY在子查询里更危险,比如UPDATE ... WHERE id IN (SELECT ... ORDER BY created_at LIMIT 10),MySQL 可能物化临时表,导致外层加锁顺序随机
SELECT FOR UPDATE 查不到记录也会死锁
在 RR 隔离级别下,SELECT ... FOR UPDATE 即使查不到任何记录,也会对查询范围加间隙锁(Gap Lock)。多个并发请求同时执行 SELECT * FROM orders WHERE order_no = 'ABC' FOR UPDATE,会争抢同一间隙,再叠加后续 INSERT 的插入意向锁,立刻构成死锁链。
- 优先用
INSERT INTO ... ON DUPLICATE KEY UPDATE替代,前提是order_no有UNIQUE约束 - 若必须用
SELECT FOR UPDATE,确保WHERE字段有唯一索引,且EXPLAIN显示type = const或ref、rows = 1 - 避免对非唯一字段(如
name)做SELECT FOR UPDATE,尤其当值重复率高时,间隙锁冲突概率陡增
LIMIT 在 UPDATE 中不提供幂等分页语义
LIMIT 在 UPDATE 中只限制影响行数,不保证分片稳定。当其他事务正在插入或删除数据时,“第 2 批 100 条”可能和上一批重叠或跳过某些记录——表面无错,但业务若依赖严格顺序处理(如消息队列消费),就会漏或重。
- 正确做法是游标式更新:每次记录上一批最大
id值作为下一批起点,例如WHERE id > 15000 AND id - 别用
OFFSET分页式分批(如LIMIT 1000 OFFSET 1000),并发执行时数据变动会导致偏移错乱 - 若必须按状态筛选,先用
SELECT id FROM ... WHERE status = 'pending' ORDER BY id LIMIT 5000拿出 ID 列表,再用WHERE id IN (...)更新——注意 MySQL 默认max_allowed_packet和IN列表长度
非主键索引更新为何更容易死锁
当 WHERE 条件走非主键索引时,InnoDB 加锁分两步:先锁非主键索引项,再回表锁主键索引。这中间存在时间窗口,若另一事务正以相反路径(比如直接用主键更新)操作同一行,就可能卡在锁获取顺序上。
- 例如事务 A 先锁
idx_status再锁PRIMARY,事务 B 同时先锁PRIMARY再锁idx_status,循环等待即刻形成 - 解决思路:优先让所有批量更新都基于主键(或唯一索引)驱动,避免混合使用不同索引路径更新同一张表
- 没有索引的
WHERE条件会退化为全表扫描,InnoDB 可能升级为“类表锁”效果,阻塞所有后续 DML
真正容易被忽略的是:死锁发生前,你往往看不到慢查询、也查不到长事务,只看到一堆线程卡在 Waiting for table metadata lock 或 Updating 状态——因为问题不在查询耗时,而在加锁路径是否可预测。每一次上线前的 EXPLAIN 验证,都是防止生产死锁的最低成本防线。











