CAP 与一致性模型:网络分区时系统究竟拒绝什么
从异步网络和不可区分故障出发,精确定义 CAP、线性一致、顺序一致、会话保证与最终一致,建立按操作选择承诺的模型。
Gilbert 与 Lynch 的 CAP 论文 在异步网络模型中形式化了一致性、可用性与分区容忍的边界;本文按操作语义使用这些定义。
CAP 的选择只在不可区分的分区窗口里发生
节点 A 收不到节点 B 的响应时,无法仅凭本地观察区分 B 宕机、网络延迟、丢包或双方仍在各自处理请求。若 A 必须在有限时间内为每个请求返回非错误结果,就可能基于旧状态接受操作;若 A 必须保证所有读写表现得像单一实时副本,就必须在无法获得足够证据时拒绝或等待。分区容忍不是可选功能,而是网络可能分裂这一运行事实。
“CP”或“AP”只是粗略标签。一个系统可以让余额扣减需要 quorum、商品描述允许陈旧读、购物车允许并发合并、权限变更在旧副本上默认拒绝。同一产品也会因读写级别、quorum、超时和冲突策略提供不同承诺。设计评审应逐操作写出失败时返回什么,而不是给数据库贴两个字母。
线性一致关心实时边界
线性一致要求每个成功操作可以放在调用开始与结束之间的某个瞬间,并且所有客户端共享一个符合现实先后关系的单一顺序。若写 W 已经向客户端返回成功,之后开始的读 R 不能看见 W 之前的值。它便于实现锁、唯一分配和条件更新,却需要更强协调并在分区时牺牲部分可用性。
顺序一致只要求所有进程看到同一操作顺序,并保持每个进程自身程序顺序,不要求这个顺序与现实时间吻合。因果一致保持 happened-before 的相关事件顺序,并发事件可以不同序。最终一致只承诺在没有新更新时副本趋于相同,若不定义冲突合并和收敛条件,这句话几乎没有工程信息。
会话保证把可理解性缩小到一个用户旅程
读己之写保证客户端提交版本 7 后不会在同一会话读回版本 5;单调读保证观察到 7 后不回退到 6;单调写保持同一会话写入顺序;写跟随读防止基于旧读覆盖更新。实现可使用会话 token、最小版本、粘性副本或在落后时转主,但每种方式都有路由和等待成本。
缓存、异步读模型和跨区域副本必须传播版本证据。只传业务值而不传 commit version,调用方无法判断新旧。降级陈旧读要带 source、version 与 age,权限、余额和配额等安全关键操作通常在证据不足时拒绝成功。
用证据边界拆开“不知道”和“做不到”
结果未知必须持久化 operationId、目标实体、预期版本和查询入口。自动重试只有在动作尚未开始或目标以相同 id 幂等时安全。用异常类名直接判断“肯定没执行”会在网络断点后制造重复提交。
状态机必须拥有终态、租约和人工出口
每个非终态都要有下一次唤醒来源:消息重投、租约到期、协调器扫描、对账任务或人工队列。只有状态而没有唤醒,流程会永久卡住;只有重试而没有终态,会永久自旋。
反向实验要验证承诺而不是验证产品名
建立两个副本与一个可控代理,先完成写 v7 并记录成功,再切断副本间链路。线性一致读应等待足够证据或失败,不能在另一个副本返回 v6 成功;允许陈旧的读则应返回 v6 + version + stale 标记。恢复网络后验证冲突策略与会话 token 让读者不回退。
实验还要区分延迟与真正分区:注入高尾延迟、单向丢包和响应丢失。若系统在 500 毫秒超时后返回错误,它仍可能满足 CAP 可用性定义之外的工程 SLO,但不能据此声称“不可用”;理论属性、业务可用与 SLO 必须分别命名。
用两个 Java 状态模型固定分布式不变量
下面的 Java 17 模型不模拟真实网络或共识实现,而是把本篇关键版本、租约、状态和容量关系变成确定输出。随后用真实数据库、Broker、锁服务或多进程环境注入延迟、重复、分区与崩溃。
javac --release 17 -Xlint:all -Werror examples/backend-development/distributed-systems/cap-consistency/PartitionDecisionDemo.java examples/backend-development/distributed-systems/cap-consistency/SessionConsistencyDemo.java
java -cp examples/backend-development/distributed-systems/cap-consistency PartitionDecisionDemo
java -cp examples/backend-development/distributed-systems/cap-consistency SessionConsistencyDemopartition=true writeQuorum=false linearizableWriteAccepted=false staleReadAccepted=true responseBounded=true
session=alice monotonicRead=true readYourWrites=true observedVersions=[5, 7, 7] regressionRejected=true输出变化时必须解释哪条一致性、epoch、幂等或容量不变量被修改。真实实验还要验证结果未知、旧所有者恢复、状态清理和人工修复路径。
容量、观测与安全要围绕协调成本
同步协调增加往返、日志刷盘、锁持有和 quorum 等待;异步收敛增加积压、版本、去重和对账存储。容量模型至少包含峰值操作率、参与者数、每次尝试、超时窗口、重试放大、状态保留、复制倍数与恢复净速率。平均值无法覆盖分区热点和长尾暂停。
反向实验要打在决定落盘的前后
故障注入必须有范围、自动停止和数据清理。生产演练从只读、单租户、单分片和低比例开始,保留旁路与回滚。完成标准不是组件重新可达,而是业务不变量保持、未知结果已对账、积压按预测下降且旧所有者无法继续写。
演进从缩小协调域开始
小规模优先单库本地事务、模块化单体和单一调度 owner。增长后先按业务冲突域分片、用批量与读模型减少跨域协调,再引入 Outbox、租约或 Saga。不要为了“分布式化”把一个本地不变量拆成跨服务事务。
任何新协调机制都写决策记录:解决什么不变量、网络分区时拒绝什么、权威证据在哪、超时后如何查询、状态保留多久、容量上限、故障演练与移除路径。系统复杂度只有在可验证收益覆盖恢复成本时才值得增加。
数据模型要为并发和恢复保留足够字段
替代方案要比较协调域而不是比较组件列表
团队门禁要阻止不可恢复的捷径
代码审查要求状态转换与 SQL 条件同时出现,集成测试要求在决定落盘前后杀进程,发布检查要求新旧版本互读,运行看板要求展示最老非终态而不只展示总数。分布式正确性不是某次设计评审的结论,而是持续被反例验证的工程属性。
