update join加锁顺序由mysql优化器决定,与sql书写顺序无关,易引发死锁;order by被驱动表主键可统一加锁路径;更可靠的是拆为单表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锁,且这些锁要等到整个事务结束才释放。
用ORDER BY id强制统一加锁路径
在必须保留JOIN语法的场景下(比如ORM限制),ORDER BY t1.id不是为了排序结果,而是向优化器“暗示”按t1主键物理顺序访问数据。这能显著提升所有事务按同一路径触发行的概率,降低加锁顺序分歧。但要注意:
- 这个
ORDER BY必须作用于被驱动表(通常是左表)的主键,不能是关联字段或非索引列 - 若
t2的关联字段没索引,优化器可能直接放弃该路径,改走全表扫描+间隙锁 - 执行前务必用
EXPLAIN FORMAT=TRADITIONAL确认t1走了主键索引,且t2走了唯一索引或至少普通索引
拆成单表UPDATE并显式控制ID顺序
比“暗示”更可靠的做法是彻底绕开JOIN:应用层先查出要更新的主键列表,再拼成IN语句批量更新。关键在于ID列表必须升序排列,且每批不超过max_allowed_packet限制(通常500个以内):
- 第一步:
SELECT t1.id FROM t1 JOIN t2 ON t1.id = t2.t1_id WHERE t2.status = 'pending' ORDER BY t1.id - 第二步:把返回的ID列表按升序分组,每组生成一条
UPDATE t1 SET ... WHERE id IN (1,2,3,...) AND id BETWEEN ? AND ? - 若
t2的t1_id字段无索引,这条SELECT本身就会全表扫描+锁大量间隙,得先补索引
最容易被忽略的锁持有时间陷阱
死锁常发生在“看起来很快”的语句上——比如UPDATE accounts JOIN user_logs只影响3行,但事务里夹着一次HTTP调用或Redis读写,导致锁持有时间从几毫秒拉长到几百毫秒。这时哪怕加锁顺序完全一致,也容易因等待时间过长触发Lock wait timeout exceeded。真正卡住系统的往往不是慢查询,而是那个在事务里默默等了8秒的外部依赖。所以务必检查事务边界:所有非DB操作必须移出BEGIN…COMMIT块。











