批量执行 UPDATE 如果缺少控制,往往会直接卡住 MySQL 复制中的 SQL 线程。根源在于大事务会导致从库 SQL 线程串行阻塞、Exec_Master_Log_Pos 长时间冻结,同时 Seconds_Behind_Master 快速飙升。要避免 MySQL 批量更新阻塞主从复制,关键做法是:按主键范围分批更新、使用显式事务、优化索引,并正确配置并行复制。

直接结论:批量 UPDATE 不加控制,几乎一定会卡住 SQL 线程——这不只是复制延迟,而是主从复制链路中的单点阻塞。
为什么大事务会让从库Seconds_Behind_Master暴涨,但Exec_Master_Log_Pos几乎不动
从库 SQL 线程回放 relay log,本质上仍然需要按顺序逐条执行。即使已经开启 MySQL 并行复制,一旦进入单个大事务内部,依旧只能串行执行到结束。举个典型场景:一条 UPDATE 一次性修改 10 万行数据,通常会持续持有大量行级 X 锁和间隙锁,短则几十秒,长则数分钟。也正因如此,这段时间里 SQL 线程常见状态往往是 Waiting for dependent transaction to commit 或 Reading event from the relay log;与此同时,Exec_Master_Log_Pos 看起来像“卡死”一样几乎不变化,而 Seconds_Behind_Master 却会以每秒 +1、+2 的速度持续上升。
这通常不是网络延迟,也不是磁盘 IO 性能不足,而是复制架构本身的串行化瓶颈。你看到的所谓“主从延迟”,本质上是从库 SQL 线程被一条大 SQL 语句长时间钉住了。
- 查证方式:在从库执行
SHOW SLA VE STATUSG,重点观察Sla ve_SQL_Running_State和Exec_Master_Log_Pos是否长时间停滞 - 确认主库是否存在活跃大事务:
SELECT trx_id, trx_started, trx_rows_modified FROM information_schema.INNODB_TRX WHERE trx_rows_modified > 10000 - 不要迷信
Seconds_Behind_Master = 0——在大事务执行期间,它可能会误报“正常”
用主键范围分批UPDATE,而不是LIMIT OFFSET分页更新
LIMIT 500 OFFSET 1000表面上像是在分页,但 MySQL 执行时仍然需要先扫描前 1500 行,锁定范围不可预测,而且 OFFSET 越大性能越差。真正更安全、更适合生产环境的方式,是基于主键值进行确定性分片。
- 先查出一批ID:
SELECT id FROM t WHERE status = 'pending' ORDER BY id LIMIT 500 - 再用
WHERE id BETWEEN ? AND ?执行更新,起始值使用上一批的MAX(id),避免遗漏或重复处理 - 每批数据量严格控制在100–500行;如果涉及多表 JOIN 或大字段更新,建议缩小到100行以内
- 每批处理后必须立即
COMMIT,并可加入SLEEP(0.01)缓解锁竞争和 CPU 压力 - WHERE条件必须可以命中索引——用
EXPLAIN确认type不是ALL,同时避免隐式类型转换(例如VARCHAR字段传入数字)
并行复制开了也会卡?重点检查这三个配置是否真正配套生效
只设置sla ve_parallel_workers > 0并不够。MySQL 并行复制要想真正发挥作用,必须满足以下三个条件:
sla ve_parallel_type必须设置为LOGICAL_CLOCK(5.7+)或WRITESET(8.0+),不能使用DATABASE——后者对单表大事务几乎没有效果- 主库
binlog_format必须为ROW,同时binlog_order_commits保持ON(默认值,切勿关闭) - 验证并行复制是否真的生效:
SELECT * FROM performance_schema.replication_applier_status_by_worker,检查LAST_SEEN_TRANSACTION是否非空;如果全部为空,通常说明并行复制并未真正启用
最容易被忽视的“伪大事务”:autocommit=1下的循环更新
代码里虽然没有显式写BEGIN,但如果在循环中持续执行INSERT/UPDATE又缺乏合理批次控制,哪怕autocommit=1下每条语句都是独立事务,整体依然可能形成“伪大事务”效应——因为网络往返、锁等待、binlog 刷盘等开销会持续累积,最终对主从复制的冲击甚至可能超过单个大事务。
这类问题在应用日志中通常不容易直接识别出“长事务”,但在从库执行SHOW PROCESSLIST时,往往能看到大量Updating状态堆积,且Exec_Master_Log_Pos只是在缓慢移动。
- 修复方式:显式
START TRANSACTION+ 批量操作 +COMMIT,把 N 个零散小事务合并成可控批次 - 避免在循环内逐行执行
UPDATE,尤其不要用SELECT ... FOR UPDATE先锁单行再更新——这类写法非常容易引发间隙锁扩散 - 所有多表操作都必须约定统一顺序(例如按表名字母顺序:
order_item → order → user),否则死锁概率会明显升高
真正难处理的,从来不只是 SQL 语法是否写对,而是必须保证每一次批量更新都能随时中断、之后还能从断点继续执行,并且一旦出现问题能够快速追溯。比如,每完成一轮分批 UPDATE,就应当把最后处理到的id记录到临时表或文件中;少了这一步,服务一旦重启,不是漏处理数据,就是重复更新数据。尤其在容器化部署环境里,这个细节最容易被忽略:配置挂载异常、临时目录未持久化、健康检查误杀仍在运行的批处理脚本……这些基础保障如果没有做好,即使批量更新方案设计得再完整,最终也很难稳定落地。
