中间件-04-MySQL读写分离后的验证方法

MySQL 读写分离上线后,最先完成的动作通常是:

  • 应用接了新的数据源配置
  • 中间件或代理层配置了主库和从库
  • 业务压测时看到主库连接数下降

这些动作只能说明“读写分离可能已经接上”,但离“验证完成”还差很远。真实项目里更常见的问题是:

  • 配置里写了读写分离,但部分读请求仍然打到主库
  • 业务压测时主库压力下降了,从库延迟却开始积累
  • 写后立刻读的场景偶发读到旧值,问题只在高峰期出现
  • 主从切换演练看似成功,结果连接池、路由缓存和只读标记没有同步收敛

这说明读写分离验证的重点,从来不只是“有没有主从”,而是下面这几件事能不能被明确证明:

  • 路由层:读请求和写请求是否真的按预期分流
  • 复制层:从库是否稳定追上主库,延迟是否在业务可接受范围内
  • 业务层:写后读、事务内读、强一致场景是否被误导到从库
  • 故障层:从库延迟、从库只读失效、主从切换时,应用是否还能给出可解释的结果

这篇文章只讨论一个核心问题:

MySQL 读写分离上线后,应该怎样设计验证路径、执行骨架和排查方法,才能真正证明它在业务场景里可用,而不只是“配置看起来正确”。

一、先把验证目标收清,不要一上来就压流量

读写分离验证最容易跑偏的地方,是一开始就只做压测,看主库和从库负载曲线有没有变化。

更合适的顺序通常是先把目标拆成四组:

1. 路由正确性

需要回答的是:

  • 普通查询是否稳定走从库
  • 插入、更新、删除是否稳定走主库
  • 显式事务中的查询会不会仍然被路由到从库
  • 带锁查询、强一致查询、写后立即读是否被错误分流

如果这一层没有证明清楚,后面所有性能数据都没有意义。

2. 复制可追赶性

需要确认:

  • 从库复制线程是否稳定
  • 高峰期延迟是否会持续累积
  • 大事务、批量更新、DDL 是否会让复制链路抖动
  • 延迟恢复速度是否符合业务容忍时间窗

3. 业务一致性边界

并不是所有读请求都适合读写分离。更需要验证的是:

  • 哪些接口允许“最终一致”
  • 哪些接口必须“写后强一致”
  • 哪些页面虽然是读接口,但结果直接影响后续业务动作

如果只按 SQL 类型分流,很容易把业务边界做错。

4. 切换与异常恢复

至少要验证下面几种场景:

  • 单个从库延迟升高
  • 单个从库不可用
  • 主从切换后旧连接是否还指向错误实例
  • 故障恢复后读流量是否能重新均衡

二、验证路径应该怎么走,才不会把问题混在一起

更适合现场执行的方式,不是一次把所有场景跑满,而是按下面 5 个阶段逐层推进:

1. 阶段一:验证实例角色和只读状态

先确认最基础的事实:

  • 当前哪台是主库,哪几台是从库
  • 从库 read_onlysuper_read_only 是否符合预期
  • 复制链路是否正常
  • 数据源配置和代理层配置是否和实际实例一致

这一阶段如果就有偏差,后面不需要继续压流量。

常见检查动作包括:

1
2
3
4
5
SHOW MASTER STATUS;
SHOW REPLICA STATUS\G
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';
SHOW VARIABLES LIKE 'server_id';

如果项目还在用旧版本语法,也可以使用 SHOW SLAVE STATUS\G,但更重要的是把这些信息收进验证记录表,而不是只在终端里看一眼。

2. 阶段二:验证路由是否按规则生效

这一步的目标不是测性能,而是证明“谁在读、谁在写”。

更实用的做法是准备 3 组最小请求:

  • 纯读请求:列表查询、详情查询、搜索查询
  • 纯写请求:新增、更新、删除
  • 写后立即读请求:提交后立刻查询结果

执行时同步观察:

  • 主库连接数和执行 SQL
  • 从库连接数和执行 SQL
  • 代理层路由日志或应用数据源日志

如果业务链路里没有直接观察入口,至少要在数据库层打开慢 SQL、general log 或代理访问日志中的一种证据来源,避免后面只能靠猜。

3. 阶段三:验证复制延迟对业务有没有实际影响

这一阶段要从“数据库延迟”翻译成“业务影响”。

更值得验证的不是单纯看 Seconds_Behind_Master,而是把它放进业务动作里:

  • 下单后立刻查订单
  • 提交配置后立刻刷新详情页
  • 更新权限后立刻访问受限资源

如果这些场景在复制延迟增加时开始出现旧值,就说明业务并不适合默认读从库。

4. 阶段四:验证高峰期读写分离是否仍然稳定

更接近真实现场的方式是混合流量验证:

  • 读多写少流量
  • 突发写入流量
  • 批量更新或大事务
  • 低频但关键的强一致查询

这一阶段重点看的是:

  • 主库 QPS 是否下降但并未失真
  • 从库 QPS、线程状态、延迟是否稳定
  • 应用是否开始出现偶发旧读、重试、超时

5. 阶段五:验证故障和切换边界

读写分离最容易在这里暴露真实问题:

  • 从库延迟持续升高时,读流量是否还继续打过去
  • 从库摘除后,路由是否自动收敛
  • 主从切换后,应用长连接是否刷新
  • 切换后只读标志、VIP、代理缓存是否一致

三、最小执行骨架:一套可以直接落地的验证方式

如果需要把读写分离验证做成固定动作,至少应该准备下面几类内容。

1. 场景清单

建议先按业务一致性要求分类,而不是按表分类:

  • 最终一致可接受场景
  • 写后必须立即可见场景
  • 事务内强一致场景
  • 权限、库存、金额、状态流转等高风险场景

这一步的目的,是先决定哪些请求可以读从库,哪些必须固定走主库。

2. 验证脚本或请求集合

至少拆成下面 4 组:

  • read_only_cases:纯查询接口
  • write_only_cases:纯写接口
  • write_then_read_cases:写后立即读
  • switch_cases:从库摘除、恢复、切换验证

如果是接口自动化体系,最好把数据准备、执行、证据采集、结果比对分开,避免一个脚本既造数据又校验延迟,最后问题无法定位。

3. 观察项

一次完整执行里至少应该同时记录:

  • 主库和从库角色信息
  • 主库与从库执行的 SQL 类型
  • 复制状态和延迟
  • 应用日志中的路由信息
  • 请求结果与业务页面表现

更推荐做一张最小记录表:

场景 请求类型 预期路由 实际路由 复制延迟 业务结果 结论
订单列表 从库 从库 0s 正常 通过
提交订单后查详情 写后读 主库或强一致读 从库 3s 读到旧值 不通过

4. 故障注入动作

如果环境允许,至少准备下面几种可控动作:

  • 限速或阻塞从库复制
  • 制造批量更新,观察复制追赶
  • 人工摘除单个从库
  • 演练主从切换

没有故障注入,验证结果通常只能停留在“平时看起来没问题”。

四、验证时最容易遗漏的 5 个重点

1. 只验证了数据库,没有验证应用侧路由缓存

很多系统在连接池、ORM、中间件代理层都做了缓存,主从角色变更后,应用未必立刻感知。

结果往往是:

  • 数据库已经切换成功
  • 应用日志里仍然连旧实例
  • 少量请求持续读旧从库

2. 只看复制延迟,没有看写后读场景

Seconds_Behind_Master0 并不代表所有请求都安全,因为:

  • 某些场景依赖更细粒度的事务提交可见性
  • 代理层可能已经把请求发到错误节点
  • 业务链路可能还依赖缓存或异步消费结果

3. 只做单条 SQL 验证,没有做接口级验证

数据库层验证能证明复制链路正常,但业务真正受影响的通常是接口和页面。

更有效的方式,是把数据库证据和业务返回一起看:

  • 数据库里已经写入成功
  • 从库还没追上
  • 页面接口提前读了从库
  • 用户看到旧状态

这四件事连起来,问题才真正闭环。

4. 忽略大事务、批量更新和 DDL

很多环境平时验证都正常,一到版本发布就出问题,原因通常不是读写分离本身,而是:

  • 批量补数拖慢复制
  • 大事务占住 apply
  • DDL 导致从库短时抖动

所以验证脚本里最好单独留一组“变更高峰”场景。

5. 切换只验证了存活,没有验证业务正确性

主从切换后看到服务恢复,只能说明链路重新连上,不能说明业务结果已经正确。

更完整的收口应该是:

  • 连接恢复
  • 路由恢复
  • 复制恢复
  • 写后读恢复
  • 高风险接口恢复

五、排查顺序:发现旧读或路由异常时先查什么

读写分离问题最怕一上来就盯着一层看。更稳的排查顺序通常是下面这样:

1. 先确认业务场景是否允许最终一致

先回答:

  • 这个接口本来是否允许从库读
  • 是否属于写后立刻读
  • 是否属于事务内读

如果业务边界一开始就划错,后面排查数据库细节也没有意义。

2. 再确认路由规则和实际路由结果

重点看:

  • 应用是否显式标记了主库读
  • 代理层是否命中了例外规则
  • 长连接是否仍然连旧实例

3. 再看复制是否跟得上

需要同步确认:

  • 复制线程是否正常
  • 延迟是否突然升高
  • 是否存在大事务、锁等待、磁盘或网络抖动

4. 最后看 SQL 和索引问题

有些“读写分离导致慢”的问题,根因并不是分离,而是:

  • 从库上索引缺失
  • 查询计划和主库不一致
  • 长查询把从库拖慢后继续放大延迟

六、真实案例:列表页偶发显示旧状态,根因并不是页面缓存

1. 场景

某业务系统上线读写分离后,工单状态更新接口压测通过,功能测试也能正常提交。但在联调阶段,列表页偶发显示旧状态,刷新几秒后才恢复。

2. 执行

现场先做了三组动作:

  • 调用“更新工单状态”接口
  • 立即调用“工单详情”和“工单列表”接口
  • 同时记录应用日志、代理层路由日志、主从复制状态

3. 现象

现象表现很一致:

  • 更新接口返回成功
  • 详情接口大多数时候正确
  • 列表接口在高峰期偶发读到旧值
  • 页面刷新 2 到 5 秒后恢复

4. 排查

按顺序排查后发现:

  • 更新请求稳定走主库,没有写丢
  • 详情接口命中了“强一致读主库”规则
  • 列表接口默认走从库
  • 高峰期从库 Seconds_Behind_Master 升到 2 到 4 秒
  • 同一时间窗口里有批量状态更新任务在跑,复制 apply 明显变慢

也就是说,问题并不是页面缓存,而是“列表接口本身被定义为普通读接口,但实际承担了写后立即确认状态的业务语义”。

5. 修复

修复动作分成两部分:

  • 调整接口策略:状态更新后的关键确认链路固定走主库,列表接口增加短时间强一致兜底
  • 调整验证基线:把“批量更新 + 写后读”纳入固定回归场景,复制延迟超过阈值时阻断默认从库读

修复后再次执行同样场景:

  • 更新后确认链路不再读旧值
  • 高峰期列表页结果稳定
  • 从库延迟仍会波动,但不再直接暴露到关键业务动作

七、这篇文章真正想落下来的结论

MySQL 读写分离后的验证,核心不是证明“架构已经搭好”,而是证明下面三件事已经被控制住:

  • 读写路由是可证明的,不是猜测的
  • 复制延迟是可观测的,不是事后才发现的
  • 业务一致性边界是明确的,不是默认所有读都能走从库

如果只做连接验证、压测验证和角色验证,读写分离大概率只能停留在“平时没事”。真正更有价值的验证方式,是把业务场景、路由证据、复制状态和故障演练收成一条完整链路。只有这样,后面出现旧读、切换异常或结果不一致时,才能快速判断问题到底出在规则设计、复制链路还是业务边界本身。