《后端架构设计·分布式与数据一致性》附录 C 分布式一致性验证清单

卷三附录 C:把单主复制切换、跨分片下单、重复消费、锁过期、时钟回拨、对账收敛六类场景写成可执行验证,每类给出前置条件、注入手段、观测信号、判定不变量、通过标准与回滚方式,并附上线前二十条检查、三态判定口径、演练顺序、数字口径与执行记录模板。

清单怎么用

这不是一张"跑一遍就打勾"的表。它的用法是:每次改动涉及复制、分片、事务、消息或锁时,从六类场景里选出受影响的两到三类,按表准备、注入、观测、判定、回滚。

使用前先确认三个前提:

  1. 每个场景都有明确的业务不变量,且不变量能写成断言;
  2. 注入手段可停止、作用范围可控;
  3. 观测信号能在同一条时间轴上对齐。

缺任何一条,验证都会退化为"看起来恢复了"。附录 A 给模型与故障语义,本附录给可执行步骤,两者配合使用。

共用前置:六类场景都要准备

项目要求不满足时的后果
环境与生产拓扑同构,至少节点角色一致结论无法外推
数据有基线快照,可重置,可重复两次结果不可比
观测指标、日志、追踪可对齐到毫秒无法判断因果
注入有开关、有范围、有停止手段演练变成事故
基线注入前记录正常指标无法判断是否恢复
回滚每步有命令、有验证、有预计耗时故障时手忙脚乱
记录专人记录时间线,不参与操作复盘时间线失真
审批影响范围、时间窗、负责人明确越权操作

数据准备容易被低估。跨分片与对账场景需要可构造的"漏单、重复、金额差、状态差",如果只能等真实流量出现,验证就无法按计划执行。正确做法是准备一组可重置的订单与流水样本,并在每次执行前恢复到已知状态。

判定不变量先写成断言

不变量断言写法检查方式
库存不超卖available >= 0 对所有 SKU 成立全量扫描或增量校验
订单状态合法每次转移属于允许边集合状态机日志校验
支付不重复每订单成功扣款次数 <= 1流水聚合
无悬挂预留预留超过补偿窗口必须已补偿或已释放按时间筛选
资金守恒扣款总额等于订单应付总额对账任务
消息副作用一次每 message_id 的业务效果计数 == 1去重表与业务表比对

断言要能直接查库或查接口,不接受"人工看一眼"。人工判断只用于分类与处置,不用于判定通过与否。

场景 1:单主复制切换

项目内容
前置条件一主多从或共识组;复制模式(同步/异步)已知;客户端能发现新主;有切换脚本与基线快照
注入手段停止主节点进程;隔离主节点网络;暂停主节点(模拟假死);注入复制延迟
观测信号主从位点差、复制延迟、选主耗时、客户端错误率、双主写入迹象、未提交事务数
判定不变量已确认提交的写不丢失;同一任期不出现两个接受写的节点;切换后读不倒退
通过标准同步复制下 RPO 目标为 0;异步复制下丢失窗口不超过声明位点差;双主窗口为 0;RTO 在演练目标内
回滚方式旧主恢复前先确认未被新主覆盖;以新主为准只读或重建;禁止双向合并

这张表里最容易写错的是"通过标准"。RPO 与 RTO 必须是团队自己确认的目标值,不能照抄别处的数字。同步复制声明 RPO 为 0,前提是提交确认确实等待了多数派;如果客户端在本地提交后就收到成功,故障切换仍可能丢尾部。

“假死"比"进程退出"更值得测。进程暂停时,健康检查可能仍认为它存活,选主不会立即发生;恢复后它可能带着旧任期继续写。判定要点不是"多久恢复”,而是"恢复后有没有被 fencing 拦住"。

场景 2:跨分片下单

项目内容
前置条件订单与库存位于不同分片;Saga 或 outbox 已接线;补偿接口幂等;可按订单号查询中间态
注入手段库存预留成功后、订单提交前崩溃;订单提交超时;补偿消息重复投递;分片路由指向旧节点
观测信号订单状态、预留记录、补偿记录、outbox 积压、悬挂时长、库存可用量
判定不变量订单存在则预留存在或已补偿;任意 SKU 的 available >= 0;无超过补偿窗口的 INVENTORY_RESERVED
通过标准最终状态属于 PAID 或 CANCELLED;补偿重试后收敛;无重复扣减;悬挂时长为 0(目标值)
回滚方式停止新单入口;按订单号重放补偿;对账修复差异;确认无在途消息后再放开

跨分片场景的难点不是"失败",而是"不知道失败"。订单提交超时后,订单可能已创建,也可能没有;库存可能已预留,也可能没有。验证时必须覆盖四种组合,而不是只测"两边都成功"和"两边都失败"。

判定时先看订单,再看预留,最后看补偿记录。如果订单不存在但预留存在,说明补偿还没跑或丢了;如果订单存在但预留不存在,说明订单侧越过了预留直接提交。两种都是不变量破坏,不能只看最终库存数。

场景 3:重复消费

项目内容
前置条件消息至少一次投递;消费端有去重表或幂等键;副作用可查询;去重窗口大于最大重试间隔
注入手段同一 message_id 重复投递(示例 3 次);副作用完成后、ack 前崩溃;重放位点回退
观测信号去重命中次数、业务表行数、资金流水条数、ack 延迟、死信数量
判定不变量每个 message_id 的业务副作用恰好一次;重复投递后聚合状态不变;流水总额等于业务总额
通过标准重复 N 次与执行 1 次结果一致(示例 N=3);无重复流水;无因重复导致的库存多扣
回滚方式消息无需回滚;修复去重表或业务数据后重放;明确记录窗口边界

重复消费的验证不能只看"第二次有没有报错"。正确做法是对比两次执行后的业务快照:订单状态、库存余量、流水条数、账户余额都必须一致。

还要测"副作用已发生但确认未发出"的窗口。消费者在写业务表和写去重表之间崩溃,恢复后消息重投,如果去重表尚未写入,副作用就会发生第二次。解决方式是把业务写入与去重写入放在同一本地事务,或者让业务操作本身带唯一约束。相关分析见 8.2 消费端幂等、去重表与恰好一次的真实边界 。

场景 4:锁过期

项目内容
前置条件锁有 TTL 与续约;带 fencing token;存储侧校验 token;被保护操作幂等或带条件写
注入手段持锁进程暂停超过 TTL;注入长 GC 或网络暂停;新持有者接管后旧持有者恢复
观测信号锁获取与释放日志、token 值、被拒绝的写次数、双写迹象、续约失败次数
判定不变量过期持有者的写被拒绝;token 单调递增;同一资源不出现并发写成功
通过标准旧持有者恢复后的写入 100% 被拒绝(目标值);无双重扣减;任一时刻至多一个有效持有者
回滚方式取消旧持有者任务;以存储侧条件写结果为准;人工核对被拒绝的写

锁过期场景的核心不是"锁会不会过期",而是"过期之后旧持有者还能不能造成影响"。如果存储侧只认"当前锁的 key 存在",旧持有者恢复后可能在新持有者持锁期间写入;只有比较 fencing token 或版本号,才能拒绝陈旧写。

本场景必须与业务不变量一起判定。锁本身没有对错,只有"锁失效时业务是否仍然正确"。这和第 10.3 锁失效后的正确性:从互斥到幂等 的结论一致。

场景 5:时钟回拨

项目内容
前置条件ID 生成器有回拨策略;事件时间与处理时间分离;TTL 与租约使用单调时钟;NTP 配置已知
注入手段将节点时钟回拨(示例 5 分钟);冻结时钟;触发 NTP 大步调整;跨节点制造偏差
观测信号ID 唯一性与单调性、事件时间与处理时间差、租约有效期、证书与 token 过期异常
判定不变量生成的 ID 不重复;订单状态不因时间倒退而回退;TTL 不提前失效也不永不过期
通过标准回拨期间 ID 唯一性 100%(目标值);无重复 ID;无非法状态转移;恢复后自动追上
回滚方式恢复系统时钟;暂停该节点发号;切换到备用 worker id 或号段;核对已发出的 ID

回拨的影响面比想象中宽:Snowflake 类 ID 可能重复,基于时间的 TTL 可能提前过期,租约可能被误判失效,按时间排序的事件流可能乱序。验证时要分别检查这四条,不能只看 ID。

“示例 5 分钟"只是注入幅度,不代表真实环境的最大偏差。真实偏差取决于 NTP 配置、虚拟化时钟和运维操作;团队应记录自己环境的历史最大偏差,并据此设计回拨策略。

场景 6:对账收敛

项目内容
前置条件双方流水可导出;差异分类规则明确;对账任务幂等;有人工处置队列与 owner
注入手段构造漏单、重复、金额差、状态差各一笔;暂停消费一段时间;让一侧写入成功、另一侧超时
观测信号差异数量与分类分布、收敛时长、自动修复率、人工队列积压、资金总额
判定不变量收敛后差异集合为空,或全部有分类与 owner;资金总额守恒;修复操作幂等
通过标准在声明的收敛窗口内差异清零(示例 T+1 或 1 小时,需业务确认);每笔差异有处置记录
回滚方式对账任务可重跑;修复操作幂等;无法自动修复的进入人工,不直接改账

对账是最后一道防线,不是唯一防线。它验证的是"前面的机制漏了之后,系统能不能发现并收敛”,因此必须故意制造差异,而不是等差异自然出现。

判定时要区分"收敛"与"掩盖":如果对账任务把差异直接改成一致,却没有留下分类与原因,下一次同类问题仍会发生。通过标准里必须包含"差异可解释",否则只是把问题藏起来。

演练顺序:先单点,后组合

六类场景不要一次全上。推荐的顺序是先验证单点机制,再验证复制与分片,最后才做组合。原因很简单:多个故障叠加后,观测信号会互相污染,归因变得不可能。

阶段场景验证重点进入下一阶段的条件
1重复消费、锁过期幂等与 fencing 是否真的生效单场景下不变量成立
2单主复制切换丢失窗口、选主、双主窗口RPO/RTO 达到声明目标
3跨分片下单中间态、补偿、悬挂无超过窗口的悬挂
4时钟回拨ID 唯一性、TTL、状态顺序无重复 ID 与非法转移
5对账收敛差异发现、分类、修复差异可解释并清零
6组合场景故障相互放大时的行为能定位到具体机制

组合场景一次只加一个变量。例如先做"复制切换 + 重复消费",确认消息在切换后重投仍幂等;再加"跨分片",观察补偿与切换是否冲突。一次叠加三个以上故障,通常只能得到"系统很乱"的结论。

数字口径:实测、目标与示例

附录里的每个数字都必须带口径,否则无法判断它能不能用于验收。

口径含义写法用途
实测在明确环境与命令下实际跑出的值写明环境、版本、命令与样本性能与容量结论
目标设计要求达到的值写明业务确认人与确认日期验收与告警阈值
示例仅用于说明量级或操作幅度明确标注"示例"讲解与演练设计
无效来源不明、环境不明、无法复现不作为任何结论应删除或重测

三条规则:

  1. 目标值不能从别处照抄,必须由业务与运维共同确认;
  2. 实测值必须附环境与命令,换机器、换版本后不能直接复用;
  3. 示例值不能进入验收标准,只能出现在讲解与注入设计里。

把"目标 RPO 为 0"写成"实测 RPO 为 0",是文档里最危险的错误之一。前者是承诺,后者是结论,两者需要的证据完全不同。

观测信号怎么对齐

验证失败最常见的原因不是系统坏了,而是信号对不上。下面五类信号要能在同一时间轴上关联:

信号关键字段对齐键常见缺失
请求时间、结果、耗时trace_id跨语言未透传
状态变更变更前后状态、原因order_id只记结果不记原因
消息生产、投递、消费、重投次数message_id未记录重投次数
锁获取、续约、释放、拒绝资源 + token只记成功不记拒绝
复制位点、任期、延迟节点 + 任期位点含义混用

对齐不了时,先修观测再演练。凭日志猜时间线,得到的结论无法复现,也无法说服别人。

上线前 20 条检查

序号检查项证据不通过时
1每个核心业务不变量有书面定义设计文档或 ADR不允许上线
2不变量有自动化检查校验任务或测试用例补检查后再上线
3幂等键作用域明确(租户/操作/请求)接口文档重新设计键
4去重窗口大于最大重试与重放间隔配置与计算说明扩大窗口或改策略
5业务表与 outbox 同事务写入代码与表结构修复事务边界
6relay 位点持久化且可重放表结构与恢复脚本补位点
7消费端去重写入与副作用原子代码审查改为同事务或唯一约束
8只对幂等操作启用自动重试重试配置清单移除不安全重试
9重试带指数退避与 jitter配置补 jitter
10超时预算逐跳递减调用链配置重设预算
11取消信号端到端传递代码与压测补传递
12分布式锁带 fencing token锁实现与存储校验补 token 校验
13租约续约窗口小于 TTL配置与时间线调整参数
14耗时测量使用单调时钟代码审查改用单调源
15ID 生成器有回拨策略设计与演练记录补策略
16复制切换的 RPO/RTO 已定义运维文档定义后再演练
17分区时读写行为明确设计文档补降级方案
18对账任务、告警、owner 就位任务配置与值班表补 owner
19六类场景至少各演练一次演练记录补演练
20回滚步骤与数据兼容性已验证回滚脚本与记录修复后再上线

二十条里,1、2、12、14、19 最容易被跳过。它们不直接产生功能,却决定故障时能不能发现问题、能不能拒绝陈旧写、能不能复现结论。

通过、失败、无效三态

验证结论只有三种,不要用"基本通过"糊过去:

状态判据处理
通过所有不变量成立,观测信号符合预期,回滚可执行记录证据,进入下一场景
失败任一不变量被破坏,或出现未解释的中间态停止扩面,定位并修复后重跑
无效注入未生效、观测缺失、环境与生产不同构修复验证条件后重跑,不计入通过

“无效"最容易被误记为"通过”。例如注入的网络分区实际没有切断流量,服务自然一切正常;或者观测指标没有采集,只能凭日志猜测。这类结果必须标记为无效,否则会给出虚假的信心。

执行记录模板

每次验证至少记录以下字段:

场景:
目标不变量:
环境与版本:
注入手段与幅度:
开始时间 / 结束时间:
观测到的关键信号:
不变量检查结果:
结论:通过 / 失败 / 无效
失败或无效的原因:
回滚是否执行、耗时:
后续改进项与 owner:

记录要能支撑另一个工程师复现同一次验证。只写"切换成功"没有价值;写清"在什么拓扑、什么复制模式、什么注入幅度下,哪个不变量在什么时间点被检查、结果如何",才有价值。

场景选择:改了什么就测什么

不必每次跑满六类。按改动触及的机制选场景,既省时间,也不遗漏关键风险:

改动至少覆盖理由
复制模式或选主参数单主复制切换丢失窗口与双主窗口会变
分片键或路由表跨分片下单、对账收敛中间态与差异分类受影响
消费逻辑或去重表重复消费副作用边界改变
锁实现或 TTL锁过期fencing 与续约关系改变
ID 生成器或 NTP 配置时钟回拨唯一性与 TTL 受影响
对账规则或修复脚本对账收敛收敛判定与幂等性改变

一次改动触及多条机制时,取并集,不要只选"最相关"的一条。例如同时改分片与消息消费,就要覆盖跨分片下单和重复消费,并额外做一次组合验证。

六个常见错误

第一,只测成功路径。跨分片下单如果只测"库存和订单都成功",永远发现不了悬挂。

第二,把超时当失败。超时是"未知",需要查询或幂等重试;直接判失败会掩盖已提交的副作用。

第三,注入不可停止。任何故障注入都必须有对应的停止手段和范围限制。

第四,没有基线。注入前不记录正常指标,事后无法判断"恢复了没有"。

第五,只验证最终数值,不验证中间态。最终库存正确,不代表过程中没有超卖;最终订单正确,不代表没有重复扣款。

第六,把一次通过当成永久结论。拓扑、版本、流量、配置变化后,原来的结论不再成立,需要重新验证。

小结

六类场景覆盖了卷三的主要风险面:复制切换看丢失窗口,跨分片下单看中间态,重复消费看副作用边界,锁过期看 fencing,时钟回拨看 ID 与 TTL,对账收敛看兜底能力。

执行时记住三句话:

  1. 先写不变量,再设计注入,最后才看指标;
  2. 目标值必须注明来源,实测值必须可复现;
  3. 通过、失败、无效三态分明,不用"基本正常"代替结论。

把这六类场景和二十条检查接进发布流程,比在事故后补文档更省成本。卷四会讨论如何把其中可自动化的部分平台化,但在那之前,先把每一次验证做成可重复、可追溯的记录。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查