多表关联更新(UPDATE ... JOIN)极易死锁,因优化器动态选择驱动表导致加锁顺序不可控;应改用显式单表操作,按ID升序分批执行并确保索引覆盖。

多表关联更新(UPDATE ... JOIN)极易触发死锁,根本原因不是语法本身,而是 MySQL 优化器决定的跨表加锁顺序不可控——你写的顺序 ≠ 实际加锁顺序,两事务稍有交错就形成 A→B→A 循环等待。必须绕开 JOIN 的隐式加锁逻辑,改用显式、可预测的单表操作路径。
为什么 UPDATE t1 JOIN t2 的加锁顺序不可信
优化器会根据统计信息、索引可用性甚至临时数据分布动态选择驱动表,事务 A 可能先锁 t2.id = 100 再锁 t1.id = 50,事务 B 却反向执行。只要两事务操作的主键/索引值存在交集,死锁立即触发。更危险的是:哪怕只更新 t1,只要 t2 出现在 ON 条件里,InnoDB 就会对 t2 中所有扫描到的行(甚至间隙)加 S 锁,且这些锁要等到整个事务结束才释放。
- 常见错误现象:
Deadlock found when trying to get lock; try restarting transaction - 使用场景:订单状态同步(
orders JOIN order_items)、用户积分批量修正(users JOIN user_logs) - 性能影响:
JOIN更新无法利用覆盖索引,常导致全表扫描 + 大量行锁,锁持有时间翻倍
用 ORDER BY id 强制被驱动表按主键顺序访问
当 ORM 或框架强制要求保留 JOIN 语法时,ORDER BY 不是为排序结果,而是向优化器“暗示”按物理顺序触发行。它能提升加锁路径一致性,但效果有限,需严格满足前提:
-
ORDER BY必须作用于被驱动表(通常是左表)的主键,不能是关联字段或非索引列 -
t2的关联字段(如t1_id)必须有索引(至少普通索引),否则优化器可能放弃该路径,改走全表扫描 + 间隙锁 - 执行前务必用
EXPLAIN FORMAT=TRADITIONAL确认t1走了主键索引,且t2走了索引(非ALL)
拆成单表 UPDATE 并按 ID 升序分批执行
这是最可靠、可验证、易监控的做法:应用层主动收拢主键列表,彻底消除跨表加锁顺序不确定性。关键不在“拆”,而在“升序”和“分批”:
- 第一步:用
SELECT id FROM t2 WHERE ... ORDER BY id拿到需更新的t1主键(ORDER BY id必须写,确保结果顺序一致) - 第二步:将 ID 列表按升序分组,每组 ≤ 500 个(避开
max_allowed_packet限制),生成形如UPDATE t1 SET x = y WHERE id IN (1,2,3,...) AND id BETWEEN ? AND ?的语句 - 若
t2.t1_id无索引,这条SELECT本身就会全表扫描 + 锁大量间隙,必须先补索引
最容易被忽略的锁持有时间陷阱
真正卡住系统的往往不是慢查询,而是那个在事务里默默等了 8 秒的 Redis 响应,或者没走索引的 SELECT FOR UPDATE——它锁的不是一行,是一整个索引区间。即使加锁顺序完全一致,锁持有时间从几毫秒拉长到几百毫秒,也会因等待超时触发 Lock wait timeout exceeded。务必检查事务边界:所有非 DB 操作(HTTP 调用、Redis 读写、日志落盘)必须移出 BEGIN…COMMIT 块。


















