长事务导致从库sql thread卡死,因row格式下需串行回放整条事务且无法拆解;无主键表引发全表扫描雪崩,子查询型update在从库二次执行加剧延迟;应改用索引驱动的确定性游标分批处理并显式commit。

长事务让从库SQL Thread卡死在一条日志上
主库一个 UPDATE 修改 50 万行,ROW 格式下 binlog 只记为一条事务日志;但从库的 SQL Thread 必须串行回放——它不能跳过、不能并发、不能拆解,只能等这整条事务执行完,才处理下一条。你看到的 Seconds_Behind_Master 突然从 0 跳到 3600,往往就是这条事务刚提交,而从库还在回放它的第 49 万行。
ROW 格式 + 无主键表 = 从库全表扫描雪崩
当表没有主键或高效二级索引时,从库回放 Update_rows_log_event 无法快速定位目标行,只能对每条变更都做一次全表扫描。10 万行更新 → 10 万次全表扫描 → I/O 和 CPU 爆涨,Exec_Master_Log_Pos 几乎不动,Slave_SQL_Running_State 长期卡在 executing event。
- 常见错误现象:
SHOW SLAVE STATUS中Seconds_Behind_Master持续上涨,但Relay_Log_Space增长缓慢,说明瓶颈不在网络或 IO Thread,而在 SQL 回放本身 - 验证方法:在从库执行
SHOW PROCESSLIST,若看到状态为Updating或executing event且Time值远大于主库对应事务耗时,基本可锁定是单事务回放阻塞 - 影响范围:即使其他表写入量很小,只要这个长事务没结束,所有后续日志都排队等待,延迟呈线性累积
LIMIT OFFSET 分批更新反而加重延迟
用 UPDATE ... LIMIT 1000 OFFSET 10000 拆分事务看似合理,实则危险:OFFSET 越大,MySQL 越要扫描前 N 行,从库回放时可能比主库还慢;更糟的是,并发执行时因 MVCC 可见性差异,容易漏行或重复更新。
- 正确做法必须用确定性游标:
WHERE id > @last_id AND status = 'pending' ORDER BY id LIMIT 1000,每次取完记录最大id作为下一轮起点 - 必须确保
id字段有索引,否则游标查询本身就会变慢,抵消拆分收益 - 每批执行后显式
COMMIT,否则仍算作同一事务,无法释放锁、无法被并行 worker 调度(即使开了slave_parallel_workers)
子查询型 UPDATE 在从库会二次执行
比如 UPDATE t1 SET a = (SELECT b FROM t2 WHERE t2.id = t1.id),ROW 格式下 binlog 记录的是每一行变更前后的镜像,但回放时从库仍需重新执行该子查询——如果 t2.id 没索引,等于在从库又做一遍 10 万行关联,CPU 和磁盘压力翻倍。
- 避免写法:改用 JOIN 更新,或先将子查询结果落临时表再关联
- 监控线索:从库
SHOW PROCESSLIST中出现大量Creating sort index或Copying to tmp table,同时Handler_read_rnd_next指标飙升 - 参数辅助:设
binlog_row_image = MINIMAL可减少 binlog 体积,但不解决子查询重执行问题











