MySQL 主从延迟致报表数据差异过大:定位与修复

问题背景

公司报表系统每天上午 9:00 自动生成前一天的日报,数据来源是 MySQL 从库(只读副本)。业务库采用一主两从架构,使用 GTID 异步复制。业务高峰期主库 QPS 约 5000,从库负责分担读负载和报表查询。

故障现象

2024 年 3 月 5 日上午,财务部门反馈 3 月 4 日的日报中订单总金额为 238 万元,但业务后台显示的金额是 271 万元,差异达到 12%。进一步比对发现,日报中缺失了大约 3 月 4 日晚上 20:00–22:00 之间的 40 多笔订单数据。

运维登录从库检查:

1
2
3
4
5
6
mysql> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
             Seconds_Behind_Master: 2147   -- 35分钟的延迟!
     Slave_SQL_Running_State: System lock
          Exec_Master_Log_Pos: 342187651
      Relay_Log_Space: 3502211072         -- 3.5GB relay log堆积

主从复制延迟 35 分钟,从库落后 9000 多个 binlog 事件未回放。

排查过程

第一步:确认延迟趋势

查看延迟变化趋势,发现并非突发:

1
$ mysql -e "SELECT * FROM mysql.slave_master_info\G" | grep -E "Retried|Heartbeat"

抄送 slave delay 监控图表显示,延迟从前一天下午 6:00 开始逐步上升,到次日凌晨 2:00 达到峰值约 50 分钟后缓慢恢复,但到上午 9:00 仍未追平。

第二步:定位延迟来源

1
2
mysql> SHOW SLAVE STATUS\G | grep -i "slave_sql_running_state"
     Slave_SQL_Running_State: System lock

从库 SQL 线程卡在 System lock 状态,说明有大事务在回放。

第三步:分析慢回放事务

查看当前的 relay log 位置对应的 binlog:

1
2
$ mysqlbinlog --base64-output=decode-rows -v mysql-bin.000287 \
    --start-position=342187651 --stop-position=342200000 | head -50

发现一个巨大的 DELETE 语句:

1
DELETE FROM order_snapshot_backup WHERE create_time < '2024-01-05 00:00:00'

该语句一次删除了 120 万行数据,在主库上执行了 2 分钟,在从库上回放需要更多时间。

第四步:确认索引缺失

1
2
3
4
mysql> SHOW CREATE TABLE order_snapshot_backup\G
-- 关键发现:create_time 列没有索引
mysql> EXPLAIN DELETE FROM order_snapshot_backup WHERE create_time < '2024-01-05 00:00:00';
-- Extra: Using where(全表扫描)

create_time 字段没有索引,120 万行的 DELETE 在主库变成全表扫描,回放到从库时 SQL 线程单线程处理,遇到全表扫描就严重阻塞。

解决方案

  1. 紧急处理
1
2
-- 在主库上添加索引(从库会自动同步DDL)
ALTER TABLE order_snapshot_backup ADD INDEX idx_create_time (create_time);
  1. 优化批量删除:将大 DELETE 拆分为小批量:
1
2
3
-- 每次删除 5000 行,间隔 1 秒
DELETE FROM order_snapshot_backup WHERE create_time < '2024-01-05' LIMIT 5000;
-- 循环执行直到 affected_rows = 0
  1. 调整删除策略:将定时清理任务从上班时间移到凌晨 2:00–4:00,避开报表查询时段。

  2. 优化从库并行复制:升级 MySQL 5.7 到 8.0 并配置 slave_parallel_workers=4,启用基于 WRITESET 的并行复制。

根因分析

直接原因:一个没有索引的全表扫描 DELETE 操作在主库执行了 2 分钟,从库 SQL 线程(单线程)在回放时遇到了相同的全表扫描,处理速度跟不上主库的写入速度,延迟逐步累积到 35 分钟。

深层原因:

  • 报表定时任务(DELETE 清理)和日报查询在同一个时间窗口竞争
  • 缺少自动删除的索引优化审查机制
  • 从库使用单线程复制(MySQL 5.7 默认),对大事务的回放能力不足

预防措施

  1. 索引审查:所有定时清理 SQL 上线前必须通过 EXPLAIN 审查,确保有合适索引
  2. 大事务拆分:任何预计影响行数 >1 万的操作必须拆分为小批量
  3. 主从延迟监控:延迟超过 60 秒立即告警,并联动检查慢查询日志
  4. 并行复制:升级到 MySQL 8.0 + WRITESET 并行复制
  5. 报表查询使用半同步复制:考虑对从库启用 rpl_semi_sync_slave 降低延迟风险

总结

主从延迟看起来是数据库问题,实际上往往是应用层 SQL 质量问题的反映。一个没有索引、不做拆分的 DELETE 语句就足以拖垮从库复制,进而影响整个报表系统的数据准确性。数据库运维不仅仅是被动监控,更需要主动审视每一个在生产环境运行的 DML 语句的质量。

使用 Hugo 构建
主题 StackJimmy 设计