跨表 UPDATE 或 DELETE 事务会锁哪些行?
MySQL 的 InnoDB 在跨表事务中不是“锁整张表”,而是按实际访问的索引记录加行级锁(
RECORD LOCK),但前提是你用的是可重复读(
REPEATABLE READ)隔离级别,且语句能走索引。如果
UPDATE t1 JOIN t2 ON ...中任意一张表扫描了全表(比如缺少关联字段索引),就可能升级为间隙锁(
GAP LOCK)甚至临键锁(
NEXT-KEY LOCK),导致意外阻塞。
实操建议:
检查执行计划:EXPLAIN FORMAT=TREE看是否走了索引,特别注意
t2表的
ON条件列是否有索引 避免在事务里写
UPDATE t1, t2 SET ... WHERE t1.id = t2.t1_id AND t2.status = 'pending'—— 若
t2.status没索引,InnoDB 可能对全表 t2 加锁 跨表操作优先拆成单表语句 + 应用层控制,比一条 JOIN 更可控
SELECT ... FOR UPDATE 跨表时锁范围怎么算?
SELECT ... FOR UPDATE在多表场景下只锁定
FROM子句中**显式引用且实际读取到的行**,不会自动延伸到 JOIN 的另一张表——除非你把它也写进
FROM或子查询中。很多人误以为
SELECT t1.* FROM t1 JOIN t2 ON t1.id = t2.t1_id FOR UPDATE会锁 t2 的行,其实不会,t2 只是被用来过滤,不参与锁定。
常见错误现象:事务 A 执行该语句后,事务 B 仍能修改 t2 的对应行,导致 A 后续 UPDATE t1 时依据的 t2 数据已变。
实操建议:
真要锁 t2 的行,得显式写进FROM:
SELECT t1.*, t2.* FROM t1 JOIN t2 ON t1.id = t2.t1_id FOR UPDATE若只关心 t1 数据一致性,但逻辑依赖 t2 的某个字段值,应在事务开始前用
SELECT ... LOCK IN SHARE MODE先锁住 t2 的相关行 注意:MySQL 8.0.22+ 支持
FOR UPDATE OF t2语法,可精确指定锁定哪张表
死锁日志里看到 “WAITING FOR THIS LOCK TO BE GRANTED” 却没报错?
这是典型的隐式锁等待,不是死锁,但容易被忽略。InnoDB 死锁检测只触发于循环等待(A 等 B、B 等 A),而跨表事务中更常见的是线性阻塞:事务 A 锁了 t1.id=100 和 t2.id=200,事务 B 先锁 t2.id=200 再试图锁 t1.id=100 —— B 会卡在第二步,但不会报死锁,只是无限等待(直到
innodb_lock_wait_timeout超时,默认 50 秒)。
性能影响明显:QPS 下降、连接堆积、监控里
innodb_row_lock_waits持续上升。
实操建议:
查实时锁等待:SELECT * FROM performance_schema.data_locks;结合
SELECT * FROM performance_schema.data_lock_waits;降低风险:跨表更新尽量按固定顺序访问表(如总是先 t1 后 t2),避免不同事务反向加锁 应用层设置合理超时:
SET innodb_lock_wait_timeout = 10(会话级),让失败更快暴露
批量跨表操作为什么越跑越慢?
不是因为数据量大,而是锁粒度随事务增长而恶化。一个事务里执行 1000 次
UPDATE t1 JOIN t2,InnoDB 会累积持有所有涉及的行锁,直到 COMMIT。期间任何其他事务只要碰其中任意一行,就阻塞。更糟的是,长事务还会拖慢 purge 线程,导致
history_list_length上升、MVCC 快照膨胀。
实操建议:
拆成小事务:每 100 行COMMIT一次,用
WHERE id BETWEEN ? AND ?分片,别用
LIMIT(易漏行) 避免在事务里做非 DB 操作(如 HTTP 请求、文件写入),否则锁持有时间不可控 确认 binlog 格式:
binlog_format = ROW是必须的,否则跨表 DML 在 statement 模式下可能主从不一致或锁范围异常
最常被忽略的一点:跨表事务的锁行为高度依赖执行计划,而执行计划又受统计信息、索引选择、优化器开关(如
optimizer_switch='index_merge=on')影响。上线前务必在生产镜像环境用真实数据压测锁表现,不能只看开发库的小表结果。
