读写分离延迟排查全流程:根因定位+并行复制调优+路由策略,实战干货一篇搞定

举报
数据库小学妹 发表于 2026/07/01 14:40:22 2026/07/01
【摘要】 读写分离架构下,从库延迟导致数据不一致怎么排查?结合真实超卖案例,从根因分析到三层治理方案,附诊断命令清单和监控告警配置。

大家好,我是数据库小学妹 👋

上个月大促那天,凌晨两点监控群突然弹了一条告警:“库存数据异常”。我连上系统看了一眼,客服后台查不到刚下的订单,仓库那边库存没扣减,前端页面还显示"有货"。就这么短短几分钟,超卖了三百多单。老板凌晨三点打电话问我怎么回事,我脑子里第一个念头是"binlog断了"。

后来查了一圈,不是断,是慢。读写分离延迟从平时的几十毫秒飙到了 3.2 秒。3秒听起来不多,但高并发场景下,3秒足够让用户看到完全过期的数据。用户下单时读到的是3秒前的库存,库存明明已经扣完了,读到的还是有货。这就是超卖的根因。

之前我们聊过主从复制的原理和搭建,也讲了主从延迟怎么监控。但延迟真正出了事、影响到业务的时候,到底怎么排查怎么治理?今天把这次排障的完整过程整理出来。有读写分离架构的朋友建议收藏,关键时刻翻出来能用。


一、延迟从哪来——四个源头叠加

读写分离的基本逻辑不难。主库负责写,从库负责读。主库的变更通过 binlog 传给从库,从库拿到 binlog 后重新执行一遍,这个重放过程就是延迟的诞生地。

第一个源头:网络传输。 binlog 从主库到从库要过网络,同城机房还好,跨地域的话单次延迟可能几十毫秒。大促期间 binlog 量暴增,传输队列一积压,延迟就往上爬。

第二个源头:从库回放瓶颈。 这是最常见的瓶颈点。主库可以并发写入,性能很高。但从库默认是单线程回放 binlog。主库一秒写入一万条,从库一秒只能回放一千条,差距越拉越大。我之前以为开了并行复制就万事大吉了,结果发现配的粒度不对,实际上还是单线程在跑。

第三个源头:大事务阻塞。 主库跑一个批量更新十万行的事务,binlog 会一次性发给从库。从库得花很长时间慢慢回放,回放期间其他小事务都得排队。

第四个源头:DDL 操作。 主库执行 ALTER TABLE 加索引,在主库上可能走 Online DDL 很快。但传到从库后,从库要重建整张表,期间所有读请求都会被阻塞。

这四个源叠加在一起,延迟就从毫秒级变成秒级,大促期间尤其明显。


二、排查过程:从报警到定位根因

接到报警后,我按"先看延迟有多大,再找延迟从哪来"的思路一步步查。

第一步:确认延迟量级

-- 在从库执行
SHOW SLAVE STATUS\G

重点看 Seconds_Behind_Master。那天查出来是 3.2 秒。

这里有个细节:Seconds_Behind_Master 是从库当前时间戳减去它正在重放的那条 binlog 事件的时间戳。如果 binlog 传输断了(网络问题或者主库 binlog 被清理了),这个值会显示 NULL。所以看到 NULL 不是延迟无限大,是同步链路断了,得先查网络。

第二步:定位阻塞源

SHOW PROCESSLIST;

从库的 SQL 线程一直在跑同一条 UPDATE。查了一下,是运营凌晨跑的批量更新活动,更新了二十万行商品数据。这个大事务还没回放完,后面所有的操作都在等它。

这里要注意一个容易搞错的点:SHOW PROCESSLIST 里看到 SQL 线程在跑,不代表它跑得快。要看 Relay_Log_SpaceExec_Master_Log_Pos 的差距,才能判断回放进度。

第三步:查并行复制配置

SHOW VARIABLES LIKE 'slave_parallel%';

结果让我有点意外。slave_parallel_type 配的是 DATABASE 级别。也就是说每个数据库只有一个回放线程。这个客户所有表都在同一个库里,所以实际上还是单线程回放。

MySQL 8.0 的并行复制有两种策略:

  • DATABASE:按库并行,不同库的事务可以并发回放。单库场景下等同单线程。
  • LOGICAL_CLOCK:按主库的并行组回放,同一组内无冲突的事务可以并发。这是真正的并行。
  • WRITESET(8.0.20+):基于行哈希判断冲突,比 LOGICAL_CLOCK 更细粒度,但对无主键表无效。

改成 LOGICAL_CLOCK 后,从库可以并发回放同一库下不同表的事务,回放能力提升好几倍。

第四步:确认 binlog 格式

SHOW VARIABLES LIKE 'binlog_format';

确认是 ROW 格式,这个没问题。但 ROW 格式有个特点:每条变更都记录完整的行数据。批量更新二十万行,binlog 体积非常大,传输和回放都更慢。如果是 STATEMENT 格式,binlog 只记录 SQL 语句本身,体积小很多,但 ROW 更安全,能避免存储过程和函数导致的复制不一致。所以不能为了延迟改回 STATEMENT,得从其他地方想办法。

第五步:应用层路由逻辑

翻了开发团队的代码,他们的读写分离路由非常简单:所有 SELECT 走从库,所有 INSERT/UPDATE/DELETE 走主库。

但有个致命问题。用户下单后,立刻查询订单状态,这个查询也走了从库。订单刚写入主库,binlog 还没传到从库,当然查不到。

这就是典型的"写完立刻读"场景。我之前做项目的时候也犯过这个错——以为读写分离中间件会自动处理这种情况,实际上大部分中间件默认不会做"会话内读主库"的判断,得自己写规则。


三、三层治理方案

排查完根因,接下来就是治理。我分了三层来做,每层解决不同的问题。

架构层:开启并行复制

STOP SLAVE;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL slave_parallel_workers = 4;
START SLAVE;

worker 数量别拍脑袋设。经验值是 CPU 核数的 1/2 到 2/3。4核机设2-3个,8核机设4-5个。设太多反而会有线程切换开销,得不偿失。

如果 MySQL 版本是 8.0.20+,可以考虑 WRITESET 模式,冲突检测更精确。但前提是所有表都有主键——没主键的表在 WRITESET 模式下哈希值算不出来,冲突检测直接失效。这又是一个"所有表必须有主键"的理由。

配置层:半同步复制兜底

并行复制解决了回放速度,但没法保证从库"跟得上"。万一网络抖动或者主库写入暴增,从库还是可能落后。这时候需要半同步复制来兜底。

半同步的机制是:主库写完数据后,至少等一个从库确认收到 binlog,才返回给应用。这样从库不会落后太多,最坏情况也就是一个网络 RTT 的延迟。

-- 主库
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000;

-- 从库
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

timeout 设为 1000 毫秒。如果从库 1 秒内没确认,主库自动降级为异步复制,不影响业务可用性。这个降级机制很重要——半同步不能变成"不同步就卡死",否则从库一挂主库也跟着瘫。

实际部署中,我习惯用 SHOW STATUS LIKE 'Rpl_semi_sync%'; 监控降级次数。如果 Rpl_semi_sync_no_tx 持续增加,说明从库经常超时,得查网络或者考虑增加从库节点。

应用层:关键读操作走主库

这是最关键的一步。不是所有读都必须走从库,有几类读操作必须读主库:

  • 写完立刻读的场景,比如下单后查订单状态
  • 强一致性要求的场景,比如支付后查余额
  • 数据量不大的读操作,主库完全扛得住

开发团队加了一个路由规则:带"强一致"标记的查询直接路由到主库,其他普通查询才走从库。改完这一行代码,超卖问题再没出现过。

这里顺便提一下,金仓数据库在读写分离集群场景下,提供了会话级的一致性读路由能力,同一会话内的写后读自动路由到主库,不用开发在应用层手动标记。这种底层能力能省不少代码,但也别完全依赖——应用层的判断逻辑还是要有的,毕竟不是所有中间件都提供这种能力。


四、延迟容忍度怎么定

这个问题没有标准答案,得看业务类型。

强一致场景,延迟容忍度是零。 用户下单、支付、扣库存,这些必须立刻读到最新数据,一秒都不能等。这类读操作直接走主库。

最终一致场景,延迟容忍度是秒级。 商品详情页的销量数字,用户看到"已售1000件"和"已售1005件",差几秒完全不影响购买决策。这类读操作走从库没问题。

离线分析场景,延迟容忍度可以到分钟级。 BI 报表、数据看板跑从库完全没问题。

判断标准其实就一句话:这个数据晚几秒读到,业务会不会出问题?会出问题就读主库,不会就交给从库。别搞一刀切。


五、日常监控怎么做

别等出事了才去查延迟,平时就得监控起来。

必看的三个指标

-- 1. 延迟秒数
SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master

-- 2. 从库回放状态
SHOW SLAVE STATUS\G
-- 关注 Slave_SQL_Running 和 Slave_IO_Running 是否都为 Yes

-- 3. 主从 binlog 位置对比
SHOW MASTER STATUS;
SHOW SLAVE STATUS\G
-- 对比 Exec_Master_Log_Pos 和主库的 Position

告警阈值建议

指标 预警线 报警线
Seconds_Behind_Master 1秒 5秒
Slave_SQL_Running / Slave_IO_Running 断开立即预警 断开立即报警
主从 binlog 位置差 超过100MB 超过500MB

定时巡检脚本

#!/bin/bash
# 每5秒检查一次延迟
while true; do
  delay=$(mysql -e "SHOW SLAVE STATUS\G" | grep Seconds_Behind | awk '{print $2}')
  echo "$(date): 延迟 ${delay}秒"
  if [ "$delay" -gt 5 ]; then
    echo "告警!延迟超过5秒!"
    # 这里可以加邮件或钉钉告警
  fi
  sleep 5
done

配合 Prometheus + mysqld_exporter 做持久化监控,Grafana 面板上同时放延迟趋势、binlog 位置差、回放速率三个指标,一目了然。


六、延迟治理避坑清单

这几年做读写分离踩过的坑不少,整理几条出来。

大事务放在业务高峰期跑。 运营同学喜欢在白天跑批量更新,觉得方便。但大事务的 binlog 在从库回放很慢,高峰期从库本来就吃紧,再来个大事务直接雪崩。批量操作一定要放到低峰期,凌晨两三点跑,没人跟你抢资源。

写完立刻读还走从库。 这个前面讲过了,但真的太多人犯这个错。用户刚注册完跳个人中心,刚下完单跳订单详情,这些场景千万别走从库。直接读主库,性能完全扛得住。

以为开了并行复制就一劳永逸。 这是最大的误区。并行复制不是万能药,它只解决回放速度,不解决网络延迟,也不解决大事务。而且并行复制本身也有坑——没主键的表在 WRITESET 模式下会出问题,worker 线程设太多反而性能下降。得配合半同步复制和应用层路由一起用,才能真正稳。

从库当备份库用。 有些团队把从库用来跑定时报表、导出任务,甚至在上面建临时表。这些操作不占用主库资源,但会消耗从库的回放资源。从库被业务查询拖慢,延迟自然就上来了。建议单独搭一个分析节点,别跟复制从库混用。

监控只看 Seconds_Behind_Master。 这个指标有个致命缺陷:它依赖从库的系统时间。如果主从服务器时钟不同步,这个值就不准。所以一定要同时看 binlog 位置差,用 Exec_Master_Log_Pos 和主库 Position 的差距来判断,这个才是客观的。


总结

读写分离延迟治理,核心就三句话:

并行复制解决回放瓶颈,别让单线程拖后腿。半同步复制做兜底,保证从库不会落后太多。应用层路由最关键,强一致读主库,最终一致走从库。

这三层配合起来,延迟基本能控制在毫秒级。只靠调数据库参数是解决不了根本问题的,得从架构、配置、应用三个维度一起动手。

你在读写分离架构里遇到过什么坑?延迟最高飙到过多少?评论区一起交流,咱们互帮互助!

我是数据库小学妹,咱们下篇见 👋

本文案例基于 MySQL 8.0,不同数据库版本略有差异。核心思路通用,具体命令请参考对应数据库文档。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。