CQRS 模式写库与读库同步延迟,如何设计补偿查询策略

解读

国内互联网面试中,这道题表面问“补偿查询”,实则考察候选人对 CQRS 最终一致性本质、分布式延迟可观测性、降级兜底与用户体验之间权衡的系统化思考。性能测试工程师必须回答三个层次:

  1. 如何量化延迟:延迟分布、P99、业务可接受窗口;
  2. 如何探测延迟:测试脚本里用何种探针、断言、指标;
  3. 如何补偿:既让业务继续,又能让测试用例通过,同时不给生产带来脏数据或雪崩风险。

知识点

  1. CQRS 读写分离模型:写库→Domain Event→MQ→读库,链路天然异步。
  2. 同步延迟产生根因:MQ 堆积、消费线程池打满、读库批量重放慢、网络抖动、GC、大事务。
  3. 延迟量化指标:写入后 T0,读到最新数据 T1,Δ=T1−T0;压测需输出 Δ 的 P50/P99/最大值。
  4. 补偿查询策略分类: a. 客户端主动重试:指数退避 + 最大重试次数 + 业务幂等键; b. 服务端兜底路由:读不到时短暂回源写库(Consistent Read),需加开关与限流; c. 版本号 / 时间戳比对:写请求返回版本号,读请求带版本号轮询,超时后降级; d. 变更通知推送:WebSocket、长轮询、Server-Sent Events,收到事件再读; e. 业务语义补偿:下单后先返回“处理中”,用户手动刷新或接收短信后再查。
  5. 性能测试设计点:
    • 注入延迟故障:使用 ChaosBlade、iptables 延迟 MQ 或读库 50~500 ms,观察补偿成功率;
    • 基准场景:固定 1 k TPS 写入,同时 5 k QPS 读取,统计 Δ 分布;
    • 容量场景:阶梯加压到 MQ 消费出现 Lag,验证补偿策略不会把写库打挂;
    • 稳定性场景:跑 8 h,持续制造 200 ms 延迟,补偿重试次数不能无限增长导致线程泄露;
    • 断言模板:99% 请求在 500 ms 内读到一致数据,剩余 1% 在 3 s 内通过补偿达到一致,超时记为失败。
  6. 监控与告警:Δ>P99 阈值、重试次数>5、写库 QPS 因回源突增 30% 即告警。
  7. 风险防控:补偿查询必须带 traceId,防止重试风暴;写库回源要加令牌桶限流,避免把写库拖垮。

答案

“补偿查询策略”分三步:量化、探测、兜底。

第一步,量化可接受延迟。与业务方确认 SLA:订单创建后 500 ms 内用户看到即可。压测脚本里在每次写请求返回瞬间记录 T0,随后在读库轮询,直到读到期望数据记录 T1,计算 Δ 并聚合 P99。脚本断言 P99≤500 ms,否则记作性能缺陷。

第二步,探测延迟发生。测试阶段用 Chaos 工具给 MQ 加 200 ms 网络延迟,制造 Lag。脚本里设置两级重试:

  1. 本地指数退避:50 ms→100 ms→200 ms,最多 3 次;
  2. 若仍不一致,触发“一致性读”开关,把该次查询路由到写库只读实例,写库限流 100 QPS,防止压垮。 整个重试过程带 traceId,方便链路追踪。

第三步,兜底与降级。对前端返回“处理中”状态码 202,并附带预计刷新时间;同时后端异步推送 WebSocket 事件,事件到达后再读读库,99% 场景在 400 ms 内可收到事件。压测用例里模拟 1 k TPS 下单,5 k QPS 查询,持续 30 min,结果:

  • 平均 Δ 120 ms,P99 450 ms;
  • 补偿触发率 1.2%,无一次回源写库限流拒绝;
  • 无线程泄露,CPU 消耗增加 <5%。 满足业务 SLA,策略上线。

拓展思考

  1. 如果读库是 Elasticsearch,同步靠 Canal→MQ→ES,延迟更大,如何在不回源 MySQL 的前提下保证搜索一致性?是否引入“写后读主分片”或“实时刷新段”?
  2. 补偿重试指数退避在突发流量下可能放大 MQ Lag,如何根据 Lag 大小动态调整退避初始值,实现“自适应退避”?
  3. 金融场景要求强一致,CQRS 仅用于读放大,测试脚本如何设计“对账”任务,在日终批量校验写库与读库数据 checksum,发现延迟导致的账差?
  4. Service Mesh 环境,Envoy 支持 Retry Policy 与 Circuit Breaker,如何把补偿策略下沉到 Sidecar,减少业务代码侵入,同时让性能测试基准与生产保持一致?