CAP 理论是理解分布式系统取舍的基础。它经常被概括为“一致性、可用性、分区容错性三选二”,这个说法便于记忆但不够严谨,也容易在真实系统设计中被误用。
更实用的理解是:当系统出现网络分区时,分布式系统必须在一致性和可用性之间做取舍。平时网络正常时,一个系统可以同时做到比较好的一致性和可用性;真正考验架构选择的,是网络异常、节点超时、跨机房通信失败、消息延迟这些情况。
CAP 分别是什么意思
CAP 包含三个概念。
Consistency,一致性,在 CAP 语境下接近线性一致性:一次写入成功后,后续读取应表现得像访问同一份按顺序更新的数据,不能从其他节点读到更旧的值。它不同于数据库 ACID 中的 Consistency。
Availability,可用性,指每个非故障节点都能在合理时间内返回响应。注意这里强调的是返回响应,不代表一定返回最新数据。
Partition Tolerance,分区容错性,指系统在网络分区发生时仍然能够继续运行。网络分区可以理解为节点之间无法通信,或者通信延迟大到系统认为对方已经不可用。
在单机系统里,不需要讨论 P,因为没有跨节点通信。但 Java 分布式系统天然存在多个进程、多个节点、多个数据库副本、多个机房,P 几乎不可避免。只要系统是分布式的,就必须默认网络会出问题。
为什么不是简单的三选二
CAP 经常被简化成 C、A、P 三选二,但工程上更准确的说法是:在发生网络分区时,如果继续对外提供服务,就可能牺牲强一致性;如果坚持强一致性,就可能拒绝部分请求,从而牺牲可用性。
也就是说,P 不是一个可以随便放弃的选项。对于真实分布式系统,网络分区一定要考虑。选择更多发生在 CP 和 AP 之间。
CP 系统优先保证一致性。网络异常时,它宁可让部分请求失败,也不返回可能错误的数据。
AP 系统优先保证可用性。网络异常时,它允许部分节点继续提供服务,但可能出现短时间数据不一致,后续通过补偿、同步、校验来恢复一致。
供应链业务里的库存扣减例子
假设有一个供应链系统,核心业务链路是:客户下单、锁定库存、生成出库任务、仓库拣货、物流发货。
其中库存是最容易体现 CAP 取舍的对象。系统有两个机房:上海机房和北京机房。为了提高访问速度,两个机房都部署了订单服务和库存服务,并且都有库存数据副本。
现在有一个 SKU:A1001,可售库存只剩 1 件。上海客户和北京客户几乎同时下单。
如果系统选择强一致性,库存扣减必须由能够形成单一提交顺序的权威节点或共识协议确认。分布式锁只有在具备正确的租约、故障转移和陈旧请求隔离机制时才能参与这一过程,不能仅凭“加了一把锁”就宣称获得强一致性。只有一个订单能成功锁定库存,另一个订单会失败或进入等待。这样可以避免并发超卖,但如果上海和北京之间网络断开,无法确认权威库存的一侧就要拒绝下单或提示系统繁忙。
这就是 CP 取舍:宁可不可用,也不能卖出不存在的库存。
如果系统选择高可用,上海机房和北京机房可以在网络分区时各自接单。两个机房都认为库存还有 1 件,于是两个订单都成功。等网络恢复后,系统发现库存被多扣了,需要做补偿:取消后下单的订单、通知客服、给用户补偿券,或者从其他仓库调拨。
这就是 AP 取舍:先保证用户能下单,再通过业务补偿处理不一致。
哪些供应链场景更适合 CP
不是所有业务都需要强一致性,但有些场景不应该轻易牺牲一致性。
第一类是库存最后一件商品的锁定。对于稀缺商品、定制件、批次严格的物料,如果超卖会带来严重履约风险,库存锁定应该偏 CP。
第二类是财务结算。供应商应付、客户应收、账期核销、发票金额,这些数据一旦错了,后续修正成本很高,甚至会影响审计。财务主账通常要选择强一致或准强一致。
第三类是审批状态。采购订单从待审到已审、已驳回、已作废,状态流转必须清晰。不能一个节点显示已审批,另一个节点还允许继续修改明细。
第四类是唯一性约束。例如采购单号、出库单号、批次号,不能因为网络分区在两个节点生成重复编号。
这些场景的共同点是:错误数据比短暂不可用更可怕。系统可以提示稍后重试,但不能让错误状态进入主流程。
哪些供应链场景可以偏 AP
也有很多场景更适合优先保证可用性。
第一类是商品基础信息展示。商品名称、图片、描述、类目、品牌等信息短时间不一致,通常不会造成严重后果。读多写少的数据可以通过缓存和异步同步提升可用性。
第二类是报表统计。当天订单量、仓库作业量、供应商履约率这些指标允许分钟级延迟。报表更关心趋势和分析,不一定要求每一次刷新都读到最新事务数据。
第三类是搜索索引。订单搜索、商品搜索、供应商搜索往往使用 Elasticsearch 等搜索引擎,索引延迟几秒或几十秒通常可以接受。
第四类是消息通知。订单创建后发送短信、站内信、邮件,如果短时间失败,可以重试或补发,不应该阻塞主交易链路。
这些场景的共同点是:短时间不一致可以被用户接受,系统可以通过最终一致性修复。
Java 系统里常见的 CAP 落地方式
在 Java 分布式系统里,CAP 不是只体现在数据库选型上,也体现在服务设计、缓存策略、消息机制和降级策略里。
对于偏 CP 的场景,单库内可以依赖事务、唯一索引、条件更新和行锁建立确定的提交顺序;跨节点则需要理解所用数据库或协调组件的复制与共识语义,例如基于 Raft 的多数派提交。普通异步主从复制不能自动等同于强一致。比如库存锁定时以 sku_id + warehouse_id 唯一标识库存行,通过带可用量条件的原子更新完成扣减,避免并发请求把库存扣成负数。
对于偏 AP 的场景,可以使用缓存、消息队列、异步任务、重试补偿、定时对账、事件溯源、最终一致性表等方式。比如订单创建后发送库存变更事件,报表系统异步消费事件生成统计结果。
关键是不要把所有业务都设计成一种取舍。供应链系统里,库存、财务、审批偏一致性;报表、搜索、通知偏可用性;订单主流程则通常是分段取舍:核心状态强一致,周边动作最终一致。
一个简单的 Java 策略 Demo
在代码里可以把不同业务能力的 CAP 取舍显式表达出来,避免所有服务都套同一种降级逻辑:
1 | public enum ConsistencyMode { |
库存锁定可以配置成偏 CP:
1 | CapabilityPolicy reserveInventory = new CapabilityPolicy( |
消息通知可以配置成偏 AP:
1 | CapabilityPolicy notifyCustomer = new CapabilityPolicy( |
调用方根据策略决定失败处理方式:
1 | public OrderResult createOrder(CreateOrderCommand command) { |
这段代码表达了一个重要原则:不是所有异常都应该用同一种方式处理。库存锁定失败会影响主交易正确性,所以必须失败返回;通知失败只影响用户触达,可以通过 outbox、消息队列和定时任务补发。
一个完整的订单链路取舍
可以把下单流程拆成几个步骤:
- 创建订单草稿。
- 校验客户、价格、合同。
- 锁定库存。
- 写入订单主表。
- 发送订单创建事件。
- 生成出库任务。
- 通知客户下单成功。
其中第 2、3、4 步属于主交易链路,应该更重视一致性。第 5、6、7 步可以更多使用消息和补偿,保证主链路响应速度和可用性。
如果库存服务暂时不可用,系统可以拒绝创建正式订单,保留草稿并提示稍后重试。如果短信服务不可用,订单仍然可以创建成功,短信后续重试。这就是按业务重要性拆分 CAP 取舍。
监控和补偿不能省略
偏 AP 的系统不能只说“最终一致”,必须设计最终怎么一致。至少需要三类能力:
- 幂等:同一个订单事件重复消费时,不能重复生成出库任务或重复扣减库存。
- 对账:定时比较订单、库存流水、出库任务之间的状态差异。
- 告警:补偿失败次数、消息堆积时间、库存差异数量超过阈值时要通知研发和业务。
例如订单创建事件已经发送,但出库任务生成失败,系统应该能通过对账任务发现“已支付订单没有出库任务”,并自动补偿或进入人工处理队列。没有这些能力,AP 设计就会从“短暂不一致”变成“永久脏数据”。
总结
CAP 理论的价值不是让我们背诵一致性、可用性、分区容错性,而是提醒我们:分布式系统一定会遇到网络异常,异常发生时必须提前决定哪些数据不能错,哪些流程不能停。
供应链系统的设计原则可以总结成一句话:核心交易数据宁可慢一点,也不要错;辅助查询和通知可以快一点,再通过最终一致性修正。
真正成熟的 Java 分布式系统,不会简单宣称自己是 CP 或 AP,而是会在不同业务边界上做不同取舍,并且把补偿、对账、幂等和监控一起设计进去。