死锁主因是事务加锁顺序不一致,而非并发高低;ORDER BY仅当配合索引且EXPLAIN验证有效时才确保加锁顺序,否则可能全表扫描导致锁序失控。

死锁不是并发高导致的,而是多个事务以不同顺序锁定同一组行时立刻触发的——哪怕只有两个事务、每秒只跑几条 UPDATE,只要加锁顺序不一致,就可能秒级死锁。
ORDER BY 不等于加锁顺序安全,必须用 EXPLAIN 验证
很多人加了 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,尤其当值重复率高时,间隙锁冲突概率陡增
批量 UPDATE 的 LIMIT 不提供幂等分页语义
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列表长度限制
多表更新必须硬编码统一加锁顺序
事务 A 先锁 users 再锁 orders,事务 B 反过来,就极易触发死锁。这不是索引或并发问题,纯粹是锁序混乱。
- 所有涉及多表更新的逻辑,必须约定同一套加锁顺序,推荐按表名字母序(如
accounts → orders → users),简单、无歧义、易审查 - 拆成单表语句并显式加锁:
UPDATE users ... LOCK IN SHARE MODE、UPDATE orders ... LOCK IN SHARE MODE,不能只靠WHERE隐式加锁 - 跨库场景下,SQL 加锁顺序完全失效,必须退到应用层协调:用幂等 key + 状态机,或 Redis 分布式锁包裹整个操作,锁 key 按固定顺序拼接,如
"lock:users:123:orders:456"
真正决定锁行为的,从来不是 SQL 文本本身,而是它背后隐式的索引选择、物化策略、是否触发锁升级——这些不会报错,也不会出现在日志里,只能靠 EXPLAIN、死锁日志中的 resource-list 或死锁图去还原。

















