文章目录
-
- 每日一句正能量
- 摘要
- [1. 背景与问题](#1. 背景与问题)
- [2. 环境与数据](#2. 环境与数据)
-
- [2.1 脱敏实验环境](#2.1 脱敏实验环境)
- [2.2 一致性分级](#2.2 一致性分级)
- [3. 复现过程](#3. 复现过程)
-
- [3.1 构造写后读不一致](#3.1 构造写后读不一致)
- [3.2 用版本号识别旧读](#3.2 用版本号识别旧读)
- [3.3 监控复制状态](#3.3 监控复制状态)
- [3.4 典型故障注入](#3.4 典型故障注入)
-
- [场景 A:复制链路网络延迟](#场景 A:复制链路网络延迟)
- [场景 B:备库磁盘 I/O 变慢](#场景 B:备库磁盘 I/O 变慢)
- [场景 C:大事务阻塞回放](#场景 C:大事务阻塞回放)
- [场景 D:主库进程终止](#场景 D:主库进程终止)
- [4. 方案实施](#4. 方案实施)
- [5. 结果对比](#5. 结果对比)
-
- [5.1 优化前](#5.1 优化前)
- [5.2 优化后](#5.2 优化后)
- [5.3 数据校验](#5.3 数据校验)
- [6. 风险与复盘](#6. 风险与复盘)
-
- [6.1 复制延迟不是单一指标](#6.1 复制延迟不是单一指标)
- [6.2 固定写后读窗口并非万能](#6.2 固定写后读窗口并非万能)
- [6.3 自动切换不等于自动安全](#6.3 自动切换不等于自动安全)
- [6.4 同步复制也有代价](#6.4 同步复制也有代价)
- [6.5 回切比切换更容易被忽视](#6.5 回切比切换更容易被忽视)
- [6.6 最终检查清单](#6.6 最终检查清单)
- 结语

每日一句正能量
与谁同行,往往决定你最终能抵达何处。
同行者不仅提供陪伴,更设定你的参照系、反馈圈和能量水平。长期与否定者同行,你会习惯自我怀疑;与探索者同行,你会觉得突破边界是常态。审视你的核心社交圈------他们是推你向上、拉你平躺,还是拽你下沉?
摘要
本文以一个"读多写少"的订单与账户查询系统为背景,讨论在 KingbaseES 主备/读写分离架构中,为什么数据库已经提交成功,用户仍可能在只读节点上暂时查不到数据;以及如何通过复制延迟监控、会话级写后读、业务分级路由、故障注入和 RTO/RPO 验证,把一致性风险控制在可解释、可观测、可回退的边界内。
1. 背景与问题
某订单平台日常请求以查询为主,读写比约为 18:1。为了降低主库压力,系统将普通列表、历史订单、经营报表等查询路由到两个只读节点,订单创建、退款、库存扣减仍落到主库。架构上线初期,吞吐量明显提升,但很快出现三类投诉:
- 用户提交订单后立即进入详情页,偶尔提示"订单不存在";
- 客服修改客户标签后刷新页面,旧标签会短暂出现;
- 主库故障切换后,部分查询返回旧状态,应用日志却显示写事务已成功。
这些问题表面上像缓存、事务或程序 Bug,实质上多数来自异步复制的时间差。写事务在主库提交,并不等于对应 WAL 已经在备库接收、落盘并回放;应用若在提交后立刻把下一次查询分发到只读节点,就可能读到旧版本。
读写分离并不天然等于"无条件扩容"。它引入了新的系统边界:
- 一致性边界:哪些请求允许最终一致,哪些必须读到刚写入的数据;
- 延迟边界:复制落后多少字节、多少秒时仍可提供只读服务;
- 故障边界:主库失联、备库延迟、网络分区、磁盘变慢时如何降级;
- 恢复边界:故障后多久恢复服务,最多允许丢失多少已确认数据。
因此,本文不把目标定义为"消灭所有延迟",而是建立一套可测试的机制:对强一致请求回主库,对弱一致请求读备库;复制延迟超过阈值时自动摘除只读节点;通过故障注入验证路由、切换、回退和数据对账流程。

2. 环境与数据
2.1 脱敏实验环境
| 项目 | 配置 |
|---|---|
| 数据库 | KingbaseES V8/V9 同类主备环境,具体函数名需按版本核对 |
| 拓扑 | 1 主库 + 2 只读节点 + 1 仲裁/管理节点 |
| 复制方式 | 物理流复制,日常异步;关键窗口可调整同步策略 |
| 应用 | Java 服务,连接池区分写库与读库 |
| 数据规模 | 订单表 3200 万行,账户表 600 万行 |
| 负载 | 峰值写入 1300 TPS,查询 2.1 万 QPS |
| 业务目标 | 普通查询允许 3 秒内最终一致;支付结果、余额、权限必须强一致 |
金仓官方读写分离资料指出,备库可以承担查询负载,但复制延迟会造成主备数据在短时间内不一致;需要严格一致性的读请求应由主节点或满足强同步要求的节点承担。官方运维文档也建议在主库查询复制状态,并根据 state、sync_state 和 WAL 差距设置告警。
2.2 一致性分级
我们没有把"所有 SELECT 都发备库"作为规则,而是先按业务后果分级。
| 等级 | 典型场景 | 一致性要求 | 默认路由 |
|---|---|---|---|
| L0 | 余额、支付结果、权限判断、库存扣减结果 | 强一致 | 主库 |
| L1 | 下单后详情、刚修改后的资料页 | 会话级写后读 | 一段时间内主库 |
| L2 | 订单列表、客服检索、个人中心历史记录 | 秒级最终一致 | 只读节点 |
| L3 | 报表、趋势、离线分析 | 分钟级最终一致 | 只读节点/汇总库 |
这个表决定了后续所有路由和故障策略。真正危险的不是有复制延迟,而是业务没有说明自己能否承受延迟。
3. 复现过程
3.1 构造写后读不一致
测试表记录业务主键、版本号和主库提交时间:
sql
CREATE TABLE rw_consistency_order (
order_id BIGINT PRIMARY KEY,
order_status VARCHAR(32) NOT NULL,
version_no BIGINT NOT NULL,
primary_commit_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
payload VARCHAR(500)
);
主库执行:
sql
INSERT INTO rw_consistency_order
(order_id, order_status, version_no, payload)
VALUES (90000001, 'PAID', 1, 'consistency-test');
COMMIT;
应用在事务提交后立即把查询发送到只读节点:
sql
SELECT order_id, order_status, version_no, primary_commit_time
FROM rw_consistency_order
WHERE order_id = 90000001;
在正常负载下,大多数查询可以立即命中;当我们制造网络抖动、备库 I/O 变慢或大事务回放时,偶尔会返回 0 行。等待数百毫秒至数秒再次查询,记录才出现。此时数据库并没有"丢数据",而是只读节点尚未回放到包含该事务的日志位置。
3.2 用版本号识别旧读
仅判断"查不到"还不够。更新场景可能返回旧版本:
sql
UPDATE rw_consistency_order
SET order_status = 'REFUNDED',
version_no = version_no + 1,
payload = 'refund-finished'
WHERE order_id = 90000001;
COMMIT;
只读节点若返回 version_no = 1,说明读取到了复制回放前的旧快照。应用可以把写事务返回的 version_no = 2 放入会话上下文,后续读取发现版本落后时自动重试主库。
3.3 监控复制状态
在主库侧,使用官方运维资料给出的视图与 LSN 差值思路:
sql
SELECT
application_name,
client_addr,
state,
sync_state,
write_lsn,
flush_lsn,
replay_lsn,
sys_wal_lsn_diff(sys_current_wal_flush_lsn(), replay_lsn) AS replay_lag_bytes
FROM sys_stat_replication;
需要注意:字节差不等于时间差。业务低峰时 100 MB WAL 可能很快追平,高峰或备库磁盘异常时 10 MB 也可能持续很久。因此我们同时记录:
- WAL 差距字节数;
- 最后回放时间与当前时间差;
- 只读节点查询成功率和 P95;
- 备库 CPU、磁盘延迟、网络重传;
- 回放进程状态;
- 节点是否处于
streaming; - 应用侧"读后回主"次数。
3.4 典型故障注入
场景 A:复制链路网络延迟
在测试环境对主备复制网卡增加延迟和丢包。预期现象:
- 主库写入正常;
replay_lag_bytes持续增加;- 只读节点返回旧数据的概率上升;
- 路由层在超过阈值后摘除该节点;
- 查询回主库或切换到健康只读节点。
场景 B:备库磁盘 I/O 变慢
限制备库磁盘吞吐,观察"接收快、回放慢"的情况。若只监控网络连接正常与否,会误判节点健康;必须同时看 flush_lsn 与 replay_lsn 的差距。
场景 C:大事务阻塞回放
单事务批量更新大量数据。备库可能长时间看不到事务中间状态,直到事务提交并完成回放。此场景说明复制延迟不能只看平均值,还应观察 P95/P99 和峰值持续时间。
场景 D:主库进程终止
停止主库进程并触发故障转移,记录:
- 故障检测时间;
- 备库提升时间;
- VIP/连接入口切换时间;
- 应用连接池恢复时间;
- 首个成功写事务时间;
- 是否存在已确认但未落到新主库的数据。

4. 方案实施
4.1 路由策略一:业务标签优先
最可靠的策略不是解析 SQL 猜测读写,而是由业务显式标记一致性等级:
java
public enum ConsistencyLevel {
STRONG, // 主库
SESSION, // 写后读窗口内主库
EVENTUAL // 健康只读节点
}
接口示例:
java
OrderDetail queryOrder(long orderId, ConsistencyLevel level) {
DataSource ds = route(level, orderId);
return orderRepository.query(ds, orderId);
}
支付、余额、权限、库存确认等接口固定 STRONG;普通历史列表使用 EVENTUAL;下单后的详情页使用 SESSION。
4.2 路由策略二:写后读窗口
每次写成功后,在用户会话或请求上下文写入"强制读主截止时间":
text
rw:sticky:user:10086 = 2026-07-18T10:00:03.250
在接下来的 2~5 秒内,该用户的关键查询回主库。窗口长度不应拍脑袋,应取复制延迟 P99 加安全余量,并区分业务:
- 订单详情:3 秒;
- 账户资料:5 秒;
- 权限变更:直接强一致,不使用短窗口;
- 普通列表:允许读备库。
该方案简单,但会增加主库读流量。应监控粘滞命中率,避免因为路由键过粗导致大量无关查询回主。
4.3 路由策略三:版本令牌
更精确的方法是写操作返回业务版本号:
json
{
"orderId": 90000001,
"version": 2
}
读请求携带 expectedVersion=2。只读节点查询到版本 1 或查不到记录时,应用立即回主库:
text
先读备库
├─ version >= expectedVersion:返回
└─ version < expectedVersion / 不存在:回主库
版本令牌适合订单状态、配置版本、账户资料等有天然版本字段的对象,比固定时间窗口更准确。
4.4 路由策略四:复制健康门禁
路由层维护每个只读节点的健康状态。示例门禁:
| 指标 | 正常 | 降权 | 摘除 |
|---|---|---|---|
| 复制状态 | streaming | 短时异常 | 非 streaming 持续 10 秒 |
| WAL 差距 | < 64 MB | 64~256 MB | > 256 MB |
| 时间延迟 | < 1 秒 | 1~3 秒 | > 3 秒 |
| 查询 P95 | < 80 ms | 80~200 ms | > 200 ms |
| 错误率 | < 0.1% | 0.1%~1% | > 1% |
阈值必须由真实写入速率、网络、磁盘和业务 SLA 决定。对低写入系统,64 MB 可能很严重;对日志吞吐很高的系统,64 MB 可能几十毫秒即可追平。
4.5 故障时的降级顺序
- 摘除延迟或不可达的只读节点;
- 把 L0/L1 请求全部路由主库;
- L2 请求优先健康只读节点,无健康节点时限流回主;
- L3 报表暂停或返回最近一次快照;
- 主库容量接近阈值时,关闭非核心大查询;
- 故障恢复后逐步放量,不立即全量恢复读流量。
4.6 部署与演练流程
部署前
- 核对主备拓扑、复制模式、VIP 和 DNS;
- 确认应用写连接与读连接隔离;
- 建立复制状态、字节差、时间差、磁盘与网络监控;
- 制定业务一致性分级表;
- 准备路由开关和回主开关;
- 校验所有只读账号没有写权限;
- 对关键表准备版本校验 SQL。
灰度阶段
- 先放历史报表与非关键列表;
- 再放普通详情页;
- 写后读和支付类接口仍回主;
- 每一阶段观察 24 小时;
- 对比主库 QPS、备库延迟、回主比例和投诉率。
故障演练
- 复制暂停;
- 网络延迟与丢包;
- 备库磁盘限速;
- 备库进程停止;
- 主库进程停止;
- 管理节点或 VIP 异常;
- 节点恢复并重新加入集群;
- 回切及数据对账。
4.7 RTO 与 RPO 的测量方法
RTO 不是"备库提升完成"的时间,而是从业务不可用开始,到应用恢复可接受读写能力的总时间:
text
RTO = 故障发现 + 仲裁决策 + 备库提升 + 入口切换
+ 连接池重连 + 应用自检 + 流量恢复
本次脱敏演练记录:
| 阶段 | 示例耗时 |
|---|---|
| 故障检测 | 8 秒 |
| 仲裁与提升 | 12 秒 |
| VIP/入口切换 | 5 秒 |
| 连接池恢复 | 9 秒 |
| 健康检查与放量 | 16 秒 |
| 总 RTO | 50 秒 |
RPO 衡量故障后最多可能丢失多少已确认数据。异步复制下,主库已提交但尚未传输/回放的 WAL 可能在主机彻底损坏时丢失;同步策略可降低 RPO,但会增加提交延迟并受备库状态影响。RPO 必须通过业务流水号、事务日志和新主库数据对账来验证,不能只看"集群切换成功"。

5. 结果对比
5.1 优化前
- 所有 SELECT 随机路由只读节点;
- 无业务一致性分级;
- 只监控节点存活,不监控复制回放;
- 下单后详情偶发空结果;
- 复制延迟时仍持续向异常节点发流量;
- 故障切换只记录数据库提升时间;
- 没有业务流水级 RPO 对账。
5.2 优化后
| 指标 | 优化前 | 优化后(脱敏示例) |
|---|---|---|
| 写后读异常率 | 0.42% | 0.006% |
| 只读节点异常摘除时间 | 人工 5~15 分钟 | 10~20 秒 |
| 强一致请求误路由率 | 不可统计 | 0 |
| 主库读 QPS 降幅 | 31% | 26% |
| 故障切换业务 RTO | 约 4 分钟 | 50 秒 |
| 异步模式演练 RPO | 未核验 | 0~若干事务,按场景记录 |
| 路由回退时间 | 人工改配置 | 30 秒内开关生效 |
主库读 QPS 降幅从 31% 回落到 26%,说明部分读请求重新回到主库,但业务一致性明显改善。读写分离的价值不应只看"主库少了多少查询",还应看系统是否在一致性、容量和恢复速度之间取得可控平衡。
5.3 数据校验
故障切换后,以业务流水表为基准核对:
sql
-- 新主库最大业务流水
SELECT MAX(biz_seq), COUNT(*)
FROM rw_consistency_order;
-- 指定时间窗口的状态分布
SELECT order_status, COUNT(*)
FROM rw_consistency_order
WHERE primary_commit_time >= :begin_time
AND primary_commit_time < :end_time
GROUP BY order_status;
-- 关键订单逐笔校验
SELECT order_id, order_status, version_no
FROM rw_consistency_order
WHERE order_id IN (:critical_order_ids);
应用侧同时核对支付渠道回执、消息队列消费位点、订单流水和库存流水,避免仅凭数据库行数判断 RPO。
6. 风险与复盘
6.1 复制延迟不是单一指标
只看字节差会遗漏"回放卡住";只看时间差会受低写入期影响;只看节点存活会把严重落后的备库当成健康节点。监控必须组合使用状态、LSN 差、回放时间、磁盘、网络和业务读后回主率。
6.2 固定写后读窗口并非万能
窗口太短仍会旧读,太长会把大量查询压回主库。高价值对象建议使用版本令牌;无法版本化的场景再使用时间窗口。
6.3 自动切换不等于自动安全
主备集群应确保同一时刻只有一个主节点。网络分区下若仲裁和隔离机制失效,可能出现脑裂并造成数据分歧。演练必须覆盖"旧主是否被隔离""恢复后如何重新加入",而不仅是验证新主能否启动。
6.4 同步复制也有代价
同步策略可以降低 RPO,但会增加事务提交链路,并可能在备库或网络异常时影响主库写入。应针对支付、核心账务窗口等场景评估,而不是把所有业务无差别改成最强同步。
6.5 回切比切换更容易被忽视
故障节点恢复后,不应立刻恢复读流量。正确顺序是:
- 确认角色和时间线正确;
- 完成增量追平或重新构建;
- 检查复制状态与延迟;
- 执行数据抽样与关键流水对账;
- 小流量恢复查询;
- 观察后再扩大权重。
6.6 最终检查清单

- 已按业务后果定义 L0~L3 一致性等级;
- 支付、余额、权限等强一致请求固定走主库;
- 写成功后具备粘滞读主或版本令牌机制;
- 每个只读节点都有复制状态、LSN 差和时间延迟监控;
- 复制延迟超过门限会自动降权或摘除;
- 只读账号不能执行写操作;
- 无健康只读节点时有明确限流与降级策略;
- 已演练网络抖动、复制暂停、磁盘变慢和主库故障;
- RTO 从业务不可用到应用恢复全链路计时;
- RPO 通过业务流水和外部回执验证;
- 旧主隔离、节点重建、重新加入和回切流程已验证;
- 所有路由策略均有可在分钟级生效的回退开关。
结语
读写分离真正困难的部分,不是把连接字符串拆成"读库"和"写库",而是承认并管理复制延迟带来的可见性差异。一个可落地的方案至少需要四个组成部分:业务一致性分级、复制健康门禁、写后读保障和故障演练。
当系统能够明确回答"这条查询为什么可以读备库""延迟到什么程度必须摘除节点""主库故障后多久恢复""最多可能丢失哪些事务"时,读写分离才从一项性能配置,变成了可审计、可验证、可回退的高可用能力。
转载自:https://blog.csdn.net/u014727709/article/details/163194598
欢迎 👍点赞✍评论⭐收藏,欢迎指正