大事务导致从库延迟飙升,因主库单条update修改50万行生成一条长事务日志,而从库sql thread默认单线程串行回放,卡住后所有日志排队;关键在于“事务内修改行数多+行锁时间长+row格式”三者叠加触发瓶颈。

大事务为什么让从库延迟飙升
主库一个 UPDATE 改 50 万行,binlog 里就是一条长事务日志;从库只能串行回放——不是它不想快,是 MySQL 的 SQL Thread 默认单线程重放,卡在这条事务上,后面所有日志都得排队。你看到的 Seconds_Behind_Master 突然跳到几千秒,往往就源于此。
关键点不是数据量大就一定慢,而是「单个事务内修改行数多 + 行锁时间长 + binlog_format=ROW」三者叠加,最容易触发同步瓶颈。
常见错误现象:
-
SHOW SLAVE STATUS里Seconds_Behind_Master持续上涨、Exec_Master_Log_Pos几乎不动、Slave_SQL_Running_State停在executing event - 主库
SHOW PROCESSLIST早结束了,但从库还卡着一条Update_rows_log_event或Query_log_event - 中继日志(
relay log)体积暴涨,Relay_Log_Space达到 GB 级
LIMIT + WHERE 分批 UPDATE 必须配游标,不能用 OFFSET
直接 UPDATE ... LIMIT 1000 OFFSET 10000 是高危操作:OFFSET 越大越慢,且并发时可能漏行或重复;ROW 格式下还会导致 binlog 体积失控。
正确姿势是用确定性游标推进:
- 先确保
WHERE条件字段有高效索引(比如status = 'pending',且status上建了索引) - 用自增主键或时间戳做游标,例如:
WHERE id > @last_id AND status = 'pending' ORDER BY id LIMIT 1000 - 每次执行后显式
COMMIT,确保每个分片是独立事务 - 取完本批最大
id,作为下一轮起点:SELECT @last_id := MAX(id) FROM orders WHERE id > @last_id AND status = 'processed' LIMIT 1
示例片段(MySQL 存储过程内):
SET @last_id = 0; WHILE (SELECT COUNT(*) FROM orders WHERE id > @last_id AND status = 'pending') > 0 DO UPDATE orders SET status = 'processed' WHERE id > @last_id AND status = 'pending' ORDER BY id LIMIT 1000; SELECT @last_id := MAX(id) FROM orders WHERE id > @last_id AND status = 'processed' LIMIT 1; COMMIT; END WHILE;
ROW 格式下避免子查询和全表扫描式 UPDATE
binlog_format=ROW 时,每行变更都会记录前镜像+后镜像。如果 UPDATE t1 SET a = (SELECT b FROM t2 WHERE t2.id = t1.id) 扫了 10 万行,binlog 就写 10 万条 Update_rows_log_event,体积暴涨,网络传输+解析都变慢。
这类语句必须拆解:
- 先用
JOIN或临时表预计算好结果,再批量UPDATE - 禁用含非确定性函数(如
NOW()、RAND())的更新逻辑,否则 ROW 日志无法精准回放 - 对大表
UPDATE,优先考虑是否能用DROP TABLE/TRUNCATE PARTITION替代(尤其日志类、流水类表)
并行复制不能替代事务拆分,但必须开
即使拆了事务,单线程 SQL Thread 仍是瓶颈。MySQL 5.7+ 必须启用并行复制:
- 设
slave_parallel_workers > 0(建议 4–8,视 CPU 核数而定) - 设
slave_parallel_type = 'LOGICAL_CLOCK'(而非默认的'DATABASE'),才能跨库并行 - 注意:并行复制依赖
GTID开启,且主库需设binlog_transaction_dependency_tracking = WRITESET(MySQL 8.0+)才能真正提升小事务吞吐
但别指望靠它“救”大事务——一个 50 万行的 UPDATE 仍会被当做一个事件单元,只能由一个 worker 处理。拆事务是前提,并行是加速器。
最容易被忽略的一点:游标推进时,如果业务允许,尽量用 created_at 或 updated_at 时间字段代替主键,更易应对主键不连续、删除导致的空洞问题;但前提是该字段有索引且单调递增趋势稳定。











