update join的加锁顺序由优化器动态决定而非sql书写顺序,易因驱动表选择不同导致事务间加锁路径相反而死锁;唯一可靠方案是拆为单表操作,按id升序分批执行并确保索引覆盖。

UPDATE JOIN 的锁获取顺序不由 SQL 书写顺序决定,而是由 MySQL 优化器生成的执行计划动态确定——你写的 UPDATE t1 JOIN t2,实际可能先锁 t2 再锁 t1,只要两事务加锁路径相反,就极易死锁。
MySQL 优化器决定加锁顺序,不是你写的表顺序
很多人误以为把主表写在 UPDATE 后面、关联表写在 JOIN 后面就能控制加锁先后,但 InnoDB 实际加锁顺序取决于驱动表选择。优化器会根据统计信息、索引可用性、数据分布甚至临时缓冲区大小(如 join_buffer_size)动态决定哪个表先扫描、哪个表后匹配。
- 事务 A 可能走
t1 → t2路径:先对t1.id = 100加 X 锁,再对t2.id = 50加 S 锁 - 事务 B 却走
t2 → t1路径:先对t2.id = 50加 X 锁,再对t1.id = 100加 S 锁 - 只要两事务操作的主键/索引值有交集(比如都涉及
t1.id=100和t2.id=50),立刻形成 A→B→A 循环等待
被驱动表 ON 字段没索引时,锁范围会爆炸
哪怕你只更新 t1,只要 t2 出现在 ON 条件里,InnoDB 就必须扫描 t2 并对其所有匹配行(甚至间隙)加 S 锁——这些锁要等到整个事务结束才释放。
- 如果
t2.t1_id没索引,优化器大概率走全表扫描,t2上锁行数 = 全表行数,而非仅关联的那几行 - 在可重复读(RR)隔离级别下,还会额外加 GAP 锁,锁定
t2中所有可能插入新记录的间隙 -
EXPLAIN FORMAT=TRADITIONAL中若看到type: ALL或type: index(非ref/range),说明锁范围已失控
STRAIGHT_JOIN 能强制表访问顺序,但不等于加锁顺序可控
用 STRAIGHT_JOIN 可以让优化器按你指定的从左到右顺序访问表(比如 UPDATE t1 STRAIGHT_JOIN t2 ON ... 确保先扫 t1),但它只影响扫描路径,不保证锁的获取时机或粒度。
- 仍需确保
t1的WHERE条件列、t2的ON列都有索引,否则即使顺序固定,t2还是会被全表扫描+全表加锁 -
STRAIGHT_JOIN会禁用部分优化器策略,可能导致执行计划变差,反而放大锁持有时间 - 它无法解决“事务里夹着 HTTP 调用”这类外部依赖导致的锁等待超时问题——锁本身可能很快,但等 Redis 响应 8 秒后,别的事务早报
Lock wait timeout exceeded了
真正可控的加锁顺序只能靠单表 + 升序 ID 批量更新
绕开 JOIN 是唯一能 100% 控制加锁行为的方式:应用层先查出目标主键列表,升序排列,再分批 UPDATE ... WHERE id IN (...)。
- 第一步必须
SELECT id FROM t2 WHERE ... ORDER BY id——ORDER BY不是为了结果排序,而是让 MySQL 按聚簇索引物理顺序扫描,避免因排序引发额外锁 - ID 列表严格升序后拼进
IN,InnoDB 会按主键顺序逐行加锁,所有事务路径完全一致 - 每批不超过 500 个 ID(避开
max_allowed_packet限制),且每批内仍保持升序;若t2.t1_id无索引,这一步SELECT自身就会死锁,务必先补索引
UPDATE JOIN 语句本身执行 3ms,但若它裹在 BEGIN 和 COMMIT 之间,中间调了一次慢接口或等了一次缓存失效,那这 3ms 的锁就拖成了 3s 的阻塞源。











