update join加锁顺序不可控,因mysql优化器决定锁表顺序与sql书写顺序无关,易致a→b→a循环等待;order by t1.id仅能“暗示”统一路径,前提是t2关联字段有索引,否则无效;更可靠的是拆为单表update并按id升序执行。

为什么UPDATE JOIN的加锁顺序不可控
MySQL优化器决定先锁哪张表、按什么范围加锁,和SQL里UPDATE t1 JOIN t2的书写顺序完全无关。事务A可能先锁t2.id = 100再锁t1.id = 50,事务B却反向执行——只要主键/索引值有重叠,立刻形成A→B→A循环等待。
更关键的是:哪怕只更新t1,只要t2出现在ON条件里,InnoDB就会对t2中所有扫描到的行(甚至间隙)加S锁,且这些锁要等到整个事务结束才释放。
常见错误现象:Deadlock found when trying to get lock; try restarting transaction
ORDER BY id能强制统一加锁路径吗
ORDER BY t1.id不是为了排序结果,而是向优化器“暗示”按t1主键物理顺序访问数据,从而提升所有事务走同一加锁路径的概率。
但这个暗示有效是有前提的:
- 被驱动表
t2的关联字段(如t2.t1_id)必须有索引,否则优化器可能放弃该路径,改走全表扫描+间隙锁 - 执行前务必用
EXPLAIN FORMAT=TRADITIONAL确认t1走了主键索引,且t2走了唯一索引或至少普通索引 -
ORDER BY在IN子句中无效,但应用层生成ID列表时确保升序即可
拆成单表UPDATE并显式控制ID顺序
比“暗示”更可靠的做法是彻底绕开JOIN:应用层先查出要更新的主键列表,再拼成IN语句批量更新。
关键在于ID列表必须升序排列,且每批不超过max_allowed_packet限制(通常500个以内):
- 第一步:
SELECT id FROM t2 WHERE ... ORDER BY id拿到需更新的t1主键(务必加ORDER BY id) - 第二步:把返回的ID列表按升序分组,每组生成一条
UPDATE t1 SET ... WHERE id IN (1,2,3) - 若
t2.t1_id无索引,这条SELECT本身就会全表扫描+锁大量间隙,得先补索引
最容易被忽略的锁持有时间陷阱
死锁常发生在“看起来很快”的语句上——比如UPDATE accounts JOIN user_logs只影响3行,但事务里夹着一次HTTP调用或Redis读写,导致锁持有时间从几毫秒拉长到几百毫秒。
这时哪怕加锁顺序完全一致,也容易因等待时间过长触发Lock wait timeout exceeded。真正卡住系统的往往不是慢查询,而是那个在事务里默默等了8秒的外部依赖。
务必检查事务边界:所有非DB操作必须移出BEGIN…COMMIT块。











