update join加锁顺序由优化器动态决定,与sql书写顺序无关;即使只更新t1,t2在on条件中也会被加s锁或间隙锁且事务提交才释放。

UPDATE JOIN 的加锁顺序由优化器决定,和 SQL 书写顺序无关
MySQL 不会因为你写了 UPDATE t1 JOIN t2 就先锁 t1 再锁 t2。优化器根据执行计划动态选择驱动表和被驱动表,进而决定哪张表先加锁、加什么类型的锁(行锁、间隙锁、next-key 锁)。事务 A 可能先锁 t2.id = 100 再锁 t1.id = 50,事务 B 却反着来——只要两事务操作的主键/索引值有交集,立刻形成循环等待。
更关键的是:即使你只更新 t1,只要 t2 出现在 ON 条件里,InnoDB 就会对 t2 中所有扫描到的行(包括未命中但落在索引范围内的间隙)加 S 锁,且这些锁要等到整个事务提交才释放。
为什么 EXPLAIN 看不出锁行为,但死锁日志里却有 t2 的锁记录
EXPLAIN 只展示查询路径,不反映锁类型和范围。而死锁日志(SHOW ENGINE INNODB STATUS)里出现 t2 的锁,说明优化器实际执行时扫描了 t2 的某段索引区间,并对其加了 S 锁或间隙锁。常见诱因包括:
-
t2的关联字段(如t2.t1_id)没有索引 → 触发全表扫描 + 大量间隙锁 -
ON条件用了函数或隐式转换(如t1.id是BIGINT UNSIGNED,t2.t1_id是VARCHAR)→ 索引失效,退化为全表扫描 -
WHERE中包含范围条件(如t2.status IN ('pending', 'processing')),但复合索引顺序错误(比如建了(status, t1_id)而非(t1_id, status))→ 无法用索引定位t1_id,只能扫全表
ORDER BY id 能强制统一加锁顺序吗
可以有限影响,但不是万能的。在必须保留 UPDATE JOIN 的场景下(如 ORM 强制生成),ORDER BY t1.id 是向优化器“暗示”按 t1 主键物理顺序访问数据,提高各事务走同一加锁路径的概率。但它生效的前提是:
-
t1必须走主键索引(用EXPLAIN FORMAT=TRADITIONAL确认key列显示PRIMARY) -
t2的关联字段(如t2.t1_id)必须有索引(至少普通索引,推荐唯一索引) - 不能有导致优化器放弃该路径的干扰项(如
WHERE中存在无法下推的复杂表达式)
如果 t2.t1_id 没索引,优化器大概率直接放弃 ORDER BY 提示,改走全表扫描,此时加锁顺序依然不可控。
真正可控的加锁顺序:拆成单表 UPDATE + 升序 ID 列表
绕过优化器不确定性最可靠的方式,是应用层自己控制。流程是:
- 第一步:
SELECT t1.id FROM t1 JOIN t2 ON t1.id = t2.t1_id WHERE t2.status = 'pending' ORDER BY t1.id(确保结果升序) - 第二步:把返回的
id列表按升序分批(每批 ≤ 500),拼成UPDATE t1 SET status = 'done' WHERE id IN (1,2,3,...) AND id BETWEEN ? AND ? - 第三步:每条单表
UPDATE只锁t1,且因id升序,所有事务按相同物理顺序触发行,彻底消除跨表加锁顺序分歧
注意:若第一步的 SELECT 本身因 t2.t1_id 缺索引而全表扫描,它也会持锁很久——所以补索引是前置动作,不是可选项。
最容易被忽略的点是锁持有时间本身:哪怕加锁顺序完全一致,只要事务里夹着一次 HTTP 请求或 Redis 调用,锁就可能从几毫秒拖到几百毫秒,触发 Lock wait timeout exceeded。所有非 DB 操作必须移出 BEGIN...COMMIT 块。











