长事务update是主从同步延迟的直接断点式阻塞源;必须用where+order by+max(id)确定性游标分批更新,禁用limit offset;row格式下子查询update需改写为join或临时表;alter table、无索引update、未commit批量导入均隐性致阻塞。

长事务 UPDATE 是主从同步延迟最直接的“断点式阻塞源”,不是慢,是卡死——SQL Thread 必须等它完整执行完才能处理下一条日志,Seconds_Behind_Master 会线性跳涨。拆分不是可选项,是必须动作。
为什么 LIMIT + OFFSET 分批 UPDATE 反而更危险
表面看 UPDATE ... LIMIT 1000 OFFSET 10000 拆了事务,但实际埋了三颗雷:
-
OFFSET越大,MySQL 越要先扫描前 N 行,主库可能勉强扛住,但从库回放时因索引失效或统计信息陈旧,扫描开销翻倍 - 并发执行时,MVCC 可见性差异会导致部分行被跳过或重复更新(尤其
status类字段在多轮间被其他事务改写) - 即使每轮加
COMMIT,OFFSET本身无法保证游标连续性,Exec_Master_Log_Pos推进不稳定,监控难定位
必须用确定性游标推进:WHERE + ORDER BY + MAX(id)
核心是让每轮查询有唯一、可复现的起点和边界,避免依赖偏移量。前提是目标字段必须有高效索引(主键最优,否则二级索引需高选择性):
- 第一轮:
UPDATE orders SET status = 'done' WHERE id > 0 AND status = 'pending' ORDER BY id LIMIT 1000 - 取最大
id:SELECT @last_id := MAX(id) FROM orders WHERE id > 0 AND status = 'done' LIMIT 1 - 下一轮:
WHERE id > @last_id AND status = 'pending' ORDER BY id LIMIT 1000 - 每次执行后必须
COMMIT,否则仍属同一事务,无法被并行 worker 调度(即使开了slave_parallel_workers)
ROW 格式下子查询型 UPDATE 必须重写
像 UPDATE t1 SET a = (SELECT b FROM t2 WHERE t2.id = t1.id) 这类语句,在 ROW 格式 binlog 中虽只记变更镜像,但从库回放时仍会重新执行子查询——如果 t2.id 没索引,等于在从库做 N 次全表扫描。
- 改用
JOIN:UPDATE t1 JOIN t2 ON t1.id = t2.id SET t1.a = t2.b WHERE t1.status = 'pending' - 或先落临时表:
CREATE TEMPORARY TABLE tmp AS SELECT id, b FROM t2 WHERE ...,再关联更新 - 验证是否生效:在从库
SHOW PROCESSLIST中不再出现长时间executing状态,且慢日志里无超 2 秒的该类语句
容易被忽略的隐性长事务陷阱
真正卡住同步的,常不是显式的 BEGIN/COMMIT 块,而是这些“看起来小”的操作:
-
ALTER TABLE:即使 MySQL 5.7+ 支持 online DDL,它仍是原子事务,binlog 中占一整条事件,从库回放期间 SQL Thread 完全阻塞 - 无索引条件的
UPDATE:UPDATE t SET x=1 WHERE create_time 若 <code>create_time无索引,等于扫全表锁全表,事务时间取决于数据页数而非行数 - 未显式
COMMIT的批量导入:业务层每 500 条INSERT封一个事务,比全量包在一个事务里更安全;但若忘记COMMIT,autocommit=0 下整个导入过程就是单一大事务
游标推进的稳定性依赖索引有效性,而索引是否生效又受统计信息影响——定期 ANALYZE TABLE 不是优化建议,是同步链路的保底操作。










