UPDATE语句中的ORDER BY不保证加锁顺序安全,必须用EXPLAIN验证是否真走索引;若WHERE无索引,会全表扫描并filesort,导致锁范围不可控且易死锁。

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不仅不防死锁,还拖慢性能——它只是排序动作,不是加锁顺序保障
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 条”可能和上一批重叠或跳过某些记录——表面无错,但业务若依赖严格顺序处理(如消息队列消费),就会漏或重。
- 更稳的做法是游标式更新:
UPDATE t SET status='processing' WHERE id > ? AND status='pending' ORDER BY id LIMIT 100,每次记录上一批最大id值作为下一批起点 - 避免在事务中混合使用
LIMIT和非确定性条件(如WHERE status IN ('a','b')),不同事务可能因数据变更导致实际锁定行集不同 - 不要把
LIMIT当作并发控制手段——它不解决加锁顺序问题,只限制影响行数
非主键索引更新为何更容易死锁?
当 WHERE 条件走非主键索引时,InnoDB 加锁分两步:先锁非主键索引项,再回表锁主键索引。这中间存在时间窗口,若另一事务正以相反路径(比如直接用主键更新)操作同一行,就可能卡在锁获取顺序上。
- 典型冲突场景:
UPDATE t SET x=1 WHERE idx_col = 5vsUPDATE t SET x=2 WHERE id = 123,前者先锁idx_col再锁id,后者反过来,极易形成循环等待 - 根本解法:尽量让批量更新走主键或覆盖索引;若必须用非主键条件,确保该字段有唯一索引,缩小锁范围
- 临时规避:把批量更新拆成单条
UPDATE并按主键排序后逐条执行(适合吞吐量可控的场景)
真正卡住人的地方,往往不是“要不要加索引”,而是“加了索引但执行计划没走”;也不是“有没有 ORDER BY”,而是“ORDER BY 被优化器无视了”。死锁日志里那些看似随机的锁等待,几乎都对应着某条 SQL 在某个时刻偏离了你预设的执行路径。


















