一次MySQL主从同步因大事务延迟导致订单数据不一致的排查实录
问题背景
周一早上 9 点 17 分,我刚端着咖啡走到工位,客服总监的电话就打了过来:”有客户投诉,昨天凌晨下的订单到现在还显示’待付款’,但钱已经扣了。再这样下去要上 12315 了。”
我们做的是 B2B 商城系统,订单链路是标准的”主库写入 → 从库读取”分离架构。业务侧前端订单查询全部走从库,结算、支付回调走主库。按理说,正常的几条订单写入,从库延迟顶多几百毫秒。
但这次不一样——运维群里同事反馈,从库 Seconds_Behind_Master 指标在凌晨 2 点之后就开始飘红,目前还在 40 分钟以上。也就是说,用户今天看到的订单状态,其实是凌晨 2 点之前的老数据。
影响范围:所有 C 端订单查询接口(订单列表、订单详情、物流状态)。涉及用户:约 1.2 万。紧急程度:P1(业务受损,尚未扩散到资损,但已接到投诉)。
故障现象
登录 Zabbix 监控大屏,几个关键指标同时告警:
1. 从库延迟(Seconds_Behind_Master)
从凌晨 2:00 开始延迟曲线从 0.5s 拉升,到 9:00 仍是 2400s(40 分钟),曲线呈”阶梯式上升”:
1 | 02:00 delay=0.8s |
2. 主库写入流量
Com_update 计数显示凌晨 1:55 - 2:35 之间有 2.3 亿行 UPDATE 操作集中在主库执行,binlog 产生量约 8.6 GB。
3. 错误日志
从库的 mysqld.log 里有大量 Multi-statement transaction required more than 'max_binlog_cache_size' bytes of storage 警告——这说明从库尝试一次性应用一个超大事务时,binlog cache 不够用。
4. 业务报错
部分订单详情接口偶发返回 ERROR 1213 (40001): Deadlock found when trying to get lock,但这是次要现象,主要问题还是延迟。
排查过程
第一步:确认从库状态
登录从库查看复制状态:
1 | mysql> SHOW SLAVE STATUS\G |
Seconds_Behind_Master 还在涨,但 Slave_SQL_Running_State 显示”Reading event from the relay log”——意味着 SQL 线程并没有卡死,是在持续追。这是典型的”大事务回放慢”症状。
第二步:定位主库上的大事务
登录主库,用 mysqlbinlog 工具扫 binlog 找最长的几个事务:
1 | mysqlbinlog --no-defaults -v -v \ |
很快就看到了一段超长事务——一个跑批任务(订单超时自动关闭)在 1:57 开始,到 2:28 结束,持续 31 分钟,涉及 800 万行订单的 UPDATE:
1 | # at 12345678 |
主库这一个事务生成的 binlog 就有 6.2 GB。
第三步:分析大事务为何慢
MySQL 主从复制的原理是:从库 SQL 线程拿到主库的 binlog event 后,单线程回放(默认配置下,MySQL 5.7 没有开启 MTS 多线程回放)。一个事务从主库传到从库,从库必须按顺序完整回放完才能进入下一个事务。
主库上,800 万行 UPDATE 因为聚簇索引和锁的原因花 31 分钟;从库上同样要 800 万行 UPDATE,但因为:
- 从库硬件配置略低(IOPS 只有主库的 60%);
sync_binlog=1、innodb_flush_log_at_trx_commit=1的双 1 配置让每行都强制刷盘;- SQL 线程是单线程,无法并行回放。
所以从库回放速度大约是主库的 1/3 —— 自然就形成了 40 分钟的延迟。
第四步:分析这个事务的合理性
继续看那段 binlog,发现这个”订单超时关闭”任务的 SQL 模式是:
1 | -- 伪代码 |
单条 SQL 一次更新 800 万行。问题就出在这里——开发同学为了图省事,用一条大 SQL 完成全表批量更新。
解决方案
紧急止血
1. 业务切流:把订单查询从从库切回主库(虽然主库压力大,但此刻一致性更重要):
1 | # 在 Nginx upstream 中临时调整 |
2. 强制让从库跳过这个大事务(不推荐为常规操作,但紧急情况下可以):
1 | -- 评估一致性影响后,决定让从库直接跳过该超大事务 |
但 SET GLOBAL sql_slave_skip_counter = 1 只能跳一个 event。我这里大事务跨多个文件,最终选择的是:让从库慢慢追回(保守但安全)。
3. 临时调大从库相关参数,提升回放速度:
1 | # my.cnf |
重启从库后,回放速度从 5000 行/秒提升到 18000 行/秒,2 小时内追平延迟。
长期修复
1. 业务侧:大事务拆小
推动开发同学把”订单超时关闭”改为分批 + 循环:
1 | // 优化后的跑批代码 |
这样每个事务只更新 1000 行,单事务 binlog 不到 1 MB,从库回放几乎无感知。
2. MySQL 配置:开启多线程复制
从库永久开启 MTS:
1 | slave_parallel_workers = 16 |
3. 监控告警
添加 Seconds_Behind_Master > 60s 的告警,避免延迟 40 分钟才发现。
4. 架构优化
考虑订单读多写少、且对一致性要求高的特点,引入 ProxySQL 做读写分离,并支持强制走主库的功能(针对订单详情这种对一致性敏感的场景)。
根因分析
这个问题的根本原因是业务开发同学没有”主从复制延迟”的意识,把单实例 MySQL 时代的”一条大 SQL 搞定一切”的习惯带到了主从架构下。
具体三个直接原因:
- 单条 UPDATE 影响 800 万行 —— 远超单事务合理大小(经验值:单事务 1 万行以内);
- 从库默认单线程回放 —— MySQL 5.7 默认
slave_parallel_workers=0,大事务没有并行能力; - 未配置主从延迟告警 —— 凌晨 2:00 延迟就开始累积,但 7 小时后才被业务反馈发现。
预防措施
1. 编码规范:禁止大事务
在 Code Review Checklist 中加入:单事务 DML 行数 ≤ 5000,binlog 大小 ≤ 10 MB。可以加 SQL 拦截器:
1 | -- 生产环境拒绝大事务的拦截逻辑(伪代码) |
2. 主从延迟全链路监控
Seconds_Behind_Master 只是粗略值。更准确的方案是使用 pt-heartbeat 工具在主库每秒写一个时间戳,从库对比时间差得到精确延迟:
1 | # 主库 |
3. 跑批任务错峰执行
把”订单超时关闭”这种大跑批放在业务低峰(比如凌晨 3:30 - 5:00),并且增加单批 sleep,避免集中打主库。
4. 关键业务强制走主库
对一致性要求极高的接口(订单详情、支付结果、账户余额),在 ProxySQL 规则里强制 mysql-default_session_target_host=master,不走从库。
5. 定期演练
每季度做一次”主库宕机,从库强制提升”演练,验证从库是否能快速接管;同时也验证大事务场景下从库的恢复能力。
总结
这次故障的教训可以总结成三句话:
第一,MySQL 主从架构下,”单条大 SQL”是定时炸弹。开发同学需要从单实例思维切换到”主从延迟敏感”思维,养成”分批 + 循环”的习惯。
**第二,监控不能只看”服务是否在线”**。Slave_IO_Running=Yes 和 Slave_SQL_Running=Yes 都是 Yes,但延迟可能已经几小时。Seconds_Behind_Master 告警必须配置,且阈值要合理。
第三,对一致性敏感的业务,不要无脑走读写分离。订单详情、支付结果这类场景,宁可让主库扛一部分读流量,也要保证数据一致性。读写分离的规则要细化到表/接口级别,不能一刀切。
最后,事后我和开发 leader 约法三章:所有大表 DML 必须经过 DBA Review,所有新上线的定时任务必须压测确认 binlog 体积。规则定下来了,但更重要的是执行。这次故障的客户投诉虽然已经处理完毕,但给了我们一个深刻的提醒:主从架构不是”装上就完事”的银弹,它是需要业务、运维、DBA 一起维护的契约。