我经历过不少分布式项目的架构评审,几乎每一轮都会有人问:这个服务到底是 CP 还是 AP?问得多了我发现,CAP 理论大概是分布式领域里被引用最多、但被用错得最离谱的一个概念。很多人能脱口而出 Consistency、Availability、Partition Tolerance,可一旦系统真的出现网络抖动,该怎么降级、该拦哪类请求、该接受多长的数据不一致窗口,反而没有人能立刻说清楚。这篇文章不打算重复教科书式的定义,我想从真实架构取舍的角度,把 CP 和 AP 在选型时真正关心的细节拆开讲:哪些场景必须用多数派共识,哪些场景应该直接放行写入、到后面再对账,以及怎么把两种模型揉进同一套系统而不被坑。
1. 先看清 CAP 三个字母背后的含义:分区不是假想敌
1.1 一致性(C)不是“数据最终相同”,而是“访问像单机一样有序”
很多人在聊 CAP 时把 C 理解成“主从最终一致也算一致”,这是第一个坑。CAP 里的 C 更接近线性一致性(linearizability):一个写操作成功返回之后,任意客户端从任意节点读,都必须能读到这个新值;所有操作的生效顺序在全局时间轴上只有一个确定的排列,跟单机执行的效果等价。
举个例子,你在订单服务里创建了一笔订单,主库返回成功。紧接着你的另一个服务去读订单库的从节点,如果读到“订单不存在”,这就是典型的线性一致性被破坏。如果业务允许这种短暂延迟,说明你并不需要 CAP 里的 C,你只是需要“最终一致”。所以讨论 CP/AP 之前,得先搞清楚:你说的“一致”到底是指强一致,还是允许几秒甚至更长的收敛窗口。这决定了后面所有选型的方向。
1.2 可用性(A)不是“服务活着”,而是“请求必须有界响应”
可用性不是简单的不宕机,而是系统在有限时间内对每个请求都给出响应——成功也好、失败也好,不能让请求悬在那里无限等待。分布式理论里对 A 的定义非常严格:只要集群仍有一个非故障节点能提供服务,用户的请求就必须能得到一个代表“成功”或“失败”的明确答复。
这个定义很容易被忽略。很多做 CP 系统的团队在发生分区时,会因为没有多数派而把写请求一直阻塞在那里,直到客户端超时。在 CAP 的定义里,超时本身算不算满足可用性?严格来说不算,因为你没有在有限时间内给出明确结果。但在工程上,超时其实是我们可以接受的下限。理解了这层差异,你才会明白:AP 系统在分区时仍然对请求负责,哪怕返回旧数据;CP 系统在分区且无多数派时,本质上是选择了“宁可不给结果,也不能给错结果”。
1.3 分区(P)是一定会发生的前提,不是可选项
网络分区不是假设,而是物理事实。交换机重启、机房光缆被挖断、容器被调度走、进程长 GC、机器断电,任何一个故障都会让节点之间的消息无法互通。只要你的系统分布在同一局域网、跨机房或者跨云,分区就是迟早的事,根本不存在“我没有分区问题”的系统。
一旦分区发生,节点之间无法确认彼此状态,你只能在这两种动作里选一个:要么强制节点的行为保持一致,宁可牺牲部分请求;要么让各节点继续干活,接受数据暂时不一致。这就是 CP 和 AP 的实质选择。我经常用一个类比:一个团队被隔离成两组,总部不允许停摆,又要求两组手上的账目在任何时刻完全相同,这在物理上是不可能的。要么停工一组保账目,要么允许两组各自记账、事后合并。
1.4 “三选二”是最大误解,真实选项只有 CP 或 AP
网上流行一个说法:CAP 是“一致性、可用性、分区容忍性三者选两个”。这句话只说对了一半。分布式系统里 P 是设计前提,不是你可以关掉的开关。你真正需要决策的是:在发生分区时,保 C 还是保 A。所以正确的描述是——分区出现时,CP 和 AP 二选一;分区没有出现时,各副本之间能正常同步,C 和 A 可以同时满足,并不需要你额外“选”。
这个认知会影响你的架构评审方式。下次再有人问“你这个系统是 CP 还是 AP”,你其实应该反问一句:你的数据在分区期间允许读到多旧?允许哪些写入失败?恢复之后怎么收敛?这些问题才是 CAP 在实践里的真正落点。
2. CP 路线的代价与收益:多数派共识如何“关掉”一部分写请求
2.1 Raft 和 ZooKeeper 的共识底座:先复制,再回答
选择 CP 的典型系统是 ZooKeeper、etcd、Consul 这类基于 Paxos/Raft 的共识系统。它们的核心逻辑是:任何写操作必须先复制到集群中的多数派节点,多数派确认之后才返回成功。我以 Raft 为例说一下它内部的工作流。
客户端把写请求发给 Leader,Leader 不马上提交,而是把这条日志广播给所有 Follower。Follower 写本地日志后回 ACK。Leader 等到超过一半节点确认,才把这条日志标记为已提交,然后返回客户端成功。之后各 Follower 再慢慢追平。这个过程保证了“任何一个已提交的数据,至少存在于多数派节点的日志里”,所以哪怕部分节点挂掉,数据也不会丢。
读操作在 Raft 里也有讲究。Leader 可以用 ReadIndex 方式保证自己读到的数据是线性一致的;但如果直接去读 Follower,Follower 可能还没追平日志,就会读到旧数据。所以很多 Raft 实现的建议是:强一致读必须打到 Leader,或者先同步一次日志索引再读。对业务来说,这意味着把 CP 用在读路径上,也需要付出额外一次协调开销。
2.2 CP 的容错数学:N 个节点到底能坏几个
CP 系统普遍采用“多数派”表决,因为多数派之间必有交集,可以避免两个分区同时各自做出互相矛盾的决策。多数派的大小是N/2 + 1(向下取整)。集群能容忍的故障数 f 满足2f + 1 ≤ N,也就是说:
- 3 节点集群,最多坏 1 个,剩下 2 个形成多数派。
- 5 节点集群,最多坏 2 个,剩下 3 个形成多数派。
- 7 节点集群,最多坏 3 个,剩下 4 个形成多数派。
这个公式决定了 CP 系统的可用性边界。一个 3 节点的 ZooKeeper,如果机房故障导致三节点被分成 1 和 2 两组,那么拥有 2 个节点的那一侧可以继续提供写服务,只有 1 个节点的那一侧无法写入。如果网络故障更极端,把三个节点弄成 1、1、1 各不相通,那整个集群都失去了多数派,所有写请求都会被拒绝。你花三台机器的成本,能承受的其实是“任意一台挂掉,系统还能继续跑”,而不是有些团队以为的“挂两台也没事”。
我在跟业务方对齐时,喜欢把这句话说透:CP 不是“永远可用”,而是“在容错边界内可用”。超出容错边界,它的行为是拒绝服务,而不是给你一个不确定的结果。这个特性恰恰是安全系统喜欢的。
2.3 CP 真正的用武之地:锁、选主、分布式事务状态机
判断一个业务场景要不要上 CP,看的是“并发决策冲突时的损失”大还是“短暂不可用的损失”大。下面几类场景,我用 CP 的意愿很强:
- 分布式锁:同一把锁如果两个客户端同时持有,就可能出现两个任务同时在处理同一条数据。锁冲突造成的双写,比锁服务不可用严重得多。
- 选主(Leader Election):一个任务只能有一个执行者,出现双主意味着双写、双调度,下游分不清听谁的。
- 配置发布:核心配置如果出现不一致,可能出现“一半节点用新配置、一半节点用旧配置”,线上行为分裂。
- 分布式事务状态机:TCC、Saga 这类事务状态如果各节点判断不一致,后续补偿根本没法做。
这些场景的共同特征是:冲突后遗症的成本极高,宁可系统短暂站出来说“我现在处理不了”,也不能给出一个模棱两可的结果。这就是 CP 的收益所在。
2.4 CP 在故障演练中的真实体感:不是全停,是“部分请求变慢或失败”
我做过几次 CP 系统故障演练,最直观的感受是:分区发生时,系统不会直接停止工作,但会表现出几个典型症状。
一个是少数派侧的写请求超时或直接返回失败。另一个是选举期间的停顿——Leader 挂了之后,Raft 或 ZAB 需要重新选主,这段时间的写请求会被积压或拒绝,通常持续几百毫秒到几秒。如果分区把集群切成了 1:1:1,那就是真正意义上的“写不可用”,你在业务日志里会看到大量“节点间心跳超时、无 Leader”的报错。
这种体验对很多业务团队来说是很难接受的,尤其是面向用户的写接口。所以 CP 系统往往部署为“核心控制面”,比如注册中心、配置中心、选主组件,而不是直接承接用户流量。如果你打算用 CP 系统直接扛用户写入,一定要先评估分区时你能否接受接口不可用。
3. AP 路线的延展性收益:分区期间继续写,把结算交给“事后对账”
3.1 AP 系统为什么能做到“分区不影响响应”
与 CP 相对,AP 系统的设计目标是:每个节点在没有协调能力的情况下,仍然可以独立接受请求。我以 Dynamo 风格的 Cassandra 举例子。一个 key 按一致性哈希落在若干节点上,每个节点都能独立接受读写。写操作发到任何一个副本,它直接落盘并返回成功,不需要等在别的节点确认。读操作也是,拿到本地副本数据就可以返回,如果发现多个副本的数据不一致,就再后台做读修复。
这样设计的好处非常明显:只要还有一个节点活着,它就能继续响应,不会因为网络分区而停摆。坏处也很明显:两个分区可能同时对同一个 key 写入不同的值,全局的“最新值”暂时无法确定。用工程的话说,系统在分区期间牺牲了全局一致性,换取了对每个请求的及时回应。
3.2 AP 避免不了的天然成本:冲突、覆盖和读旧
AP 系统的运行必然伴随着数据冲突。最常见的是两个客户端在不同节点上更新同一个 key,各自都成功了,但最终保留哪个值没有被定义清楚。很多系统用“最后写入者胜(LWW)”来解决,也就是比较时间戳,后写的覆盖先写的。但 LWW 有一个隐藏风险:如果各节点的时钟没对齐,偏慢的服务器会打出一个更早的时间戳,导致本应覆盖旧值的写入被旧值覆盖掉。
你还要面对读旧数据的问题。读请求落在落后副本上,返回的是几分钟前的数据。如果这个数据是库存、状态机这类强约束字段,业务逻辑很可能被误导。所以 AP 不是“免维护”,它把一致性压力从运行期转移到了设计期——你必须为每一个可能冲突的数据预先设计好合并规则。
3.3 让 AP 数据最终收敛的手段:向量时钟、CRDT、读修复
好在工程上积累了不少把 AP 收敛做稳的办法。
向量时钟是一种比时间戳更靠谱的排序方式,它记录的是“每个节点各自接受过哪些版本的写”,冲突时通过比较版本向量判断新旧。如果两个版本在向量上互相不领先,就说明发生了真正的并发冲突,系统会把这个冲突交给应用层合并。Dynamo 里就是这么处理的。
**CRDT(无冲突复制数据类型)**是另一条路,代表有 G-Counter、PN-Counter、OR-Set 等。它们用数学上可合并的数据结构实现无冲突并发写,任何两个副本上的操作,合并后结果都是确定性的,不需要额外协调。比如 G-Counter 用每个节点自己的计数位相加,并发加一操作合并后总和正确。
Cassandra 还有读修复(Read Repair)和反熵(Anti-Entropy)。读请求发现多个副本值不一致,就用读到的较新版本覆盖旧副本;后台还会定期比对副本差异,把落后副本追平。这些机制合在一起,才是“最终一致”落地成型的工程闭环。
3.4 选 AP 的关键判断:冲突能被业务消化吗
我推荐 AP 的场景一般满足一个条件:并发写冲突在业务语义上是可合并的,或者读旧数据的代价可控。最典型的是购物车、点赞数、浏览历史、在线状态、监控指标这类数据。购物车两个终端同时加商品,本质是“集合的并集”,合并时把两边的商品都保留就行;点赞数是数字累加,用计数器或者 CRDT 合并不会出错。
反过来,余额、库存、订单状态、优惠券核销这类数据,字段之间强约束,并发写入不是简单“并集”能覆盖的。如果强行用 AP,你就要处理超卖、重复扣减、状态回退这些更头疼的问题,最终对账成本甚至超过 CP 拒绝请求的成本。所以 AP 的真正适用对象是“低价值冲突”的数据,而不是所有数据。
4. 注册中心、配置中心的选型:微服务架构里最醒目的 CP/AP 战场
4.1 ZooKeeper 做服务注册:强一致带来的可用性边界
在微服务架构里,大家最容易接触到 CAP 的场景就是服务注册中心。用 ZooKeeper 做注册中心,走的是 CP 路线:服务实例节点通过临时节点注册,客户端通过 Watcher 感知节点变化。ZooKeeper 需要超过一半节点存活且能互达,才能提供服务注册和发现能力。
假设一个 3 节点的 ZK 集群,某个网络分区把节点分成 1 和 2。那么含有 2 个节点的一侧仍然能注册新服务,只有 1 个节点的那一侧就不能注册了。此时,服务消费者如果被路由到少数派一侧,会发现服务列表明明没问题,但新服务死活拉不到。这就是 CP 注册中心的真实体验:你对服务数据的强一致得到了保证,但前提是接受“少数派侧无法更新数据”的不可用窗口。
4.2 Eureka 的选择:自我保护模式下的 AP 思路
Eureka 是另一个典型,它选择的是 AP。Eureka Server 之间通过心跳互相复制注册表,每个节点都保存全量服务信息。客户端拿到任意一个 Eureka Server 的注册表就能用,不需要多数派确认。
最生动的是 Eureka 的自我保护模式:如果短时间内丢失大量客户端心跳,Eureka Server 会假设是网络抖动而不是服务全部宕掉,于是不再剔除任何实例,保住当前注册表继续对外提供查询。这意味着所有查询请求都还能得到响应,但响应里可能包含已经宕机的服务实例。代价是调用方如果拿到一个死实例,会请求失败。解决方式靠的是客户端缓存、重试和负载均衡策略,而不是注册中心帮你把坏节点摘掉。
我在工程实践里非常认同这个思路:服务发现这个场景,本质上是“读到旧列表但可以让客户端重试换节点”好过“新服务注册不上导致流量雪崩”。所以 Spring Cloud 场景下,普通服务发现我通常不要求线性一致,Eureka 的 AP 模型更符合实际运维需要。
4.3 混用策略:锁用 CP,服务发现用 AP
现实中并不需要一个注册中心解决所有问题。我在治理平台里常做的组合是:分布式锁和集群选主走 ZooKeeper 或 etcd,普通服务注册发现走 Eureka 或 Nacos 的 AP 模式。锁和选主不允许双主,必须用 CP;服务发现允许短时间读到旧列表,因为客户端重试成本远低于注册中心不可用的成本。
这种混用有个好处:故障域被切开了。锁系统出问题时,受影响的是少数需要互斥的定时任务;注册中心出问题时,服务消费者依然可以用本地缓存继续路由,不会全链路雪崩。如果你只用一个 CP 组件承接所有治理功能,那么集群抖动一次,所有服务的注册、发现、锁、配置全部陪你一起停摆,这个风险在设计阶段就要意识到。
5. 把 CAP 从“非此即彼”变成“可控区间”的工程手段
5.1 NWR 模型:一致性可以不是全有或全无
CAP 给出的是一对绝对约束,但工程里的系统往往更灵活。Dynamo 风格的 NWR 模型就是一个旋钮。N 是副本数,W 是写需要确认的节点数,R 是读需要读取的节点数。只要满足W + R > N,读节点集合和写节点集合必有交集,读请求就能保证看到最新写入;如果不满足,读可能命中旧副本。
这个模型用得非常广。Cassandra 里,你可以配置N=3, W=2, R=1追求更快的读,或者W=3, R=1更偏重写一致性,也可以W=1, R=3更偏重读一致性。它不是非黑即白,而是在一致性、延迟、吞吐之间找平衡。如果你觉得 CP 太苛刻、AP 太松,NWR 就是你的中间地带。
5.2 会话一致性:不强一致,但让用户“感觉自己是对的”
很多业务要的不是全局强一致,而是“用户自己的操作必须对自己可见”。这可以通过会话一致性来保证。读己之写(Read-Your-Writes):用户写完数据后,紧接着的读取必须命中写入节点或已同步副本,避免用户刚提交的内容刷新就不见了。单调读:用户的后续读取结果不能比之前旧,不能这次看到新值、下次又看到旧值。
实现上很简单,比如给用户请求的 cookie 里带上最近写过的节点 ID,路由层再把该用户的读请求优先打向那个节点;或者把主从同步延迟做监控,落后超过阈值的从节点不参与用户读路由。这样你虽然整体是 AP 最终一致模型,但用户体感上很少会感知到不一致。这是一条性价比极高的优化路径。
5.3 最终一致不是放弃一致,而是要对账闭环
AP 系统能不能把账算清,完全取决于你的事后闭环是否完整。我参与过的一个订单系统就是典型 AP:下单扣库存允许短暂超卖,后台有一个对账任务每分钟扫描库存副本之间的差异,发现超卖后自动取消最后写入的订单,并给用户发通知退款。这里的关键是把“冲突解决方案”当作主业务流程的一部分来设计,而不是出了问题再人工修。
落地时要做到三件事:写操作幂等,重放不会产生副作用;补偿动作有明确的触发条件;对账任务能看到全量数据差异,不能只处理增量。只要这三件事成立,最终一致的窗口期再长,系统也能自我收敛;否则长期挂着不一致数据,后面处理一次事故的成本,远远大于当初选 CP 多付出的机器成本。
5.4 同一套系统里做数据维度拆分:CP 稳核心,AP 撑流量
我最后想强调一个实践观点:CAP 选择可以细到“数据维度”。一个系统里,核心交易库用 CP 思路保证不丢不重,读多写少的缓存、计数、推荐列表用 AP 思路扛高并发,中间用消息队列和异步对账衔接。
比如库存商品信息在主库里保证强一致,而商品详情页的点击量、浏览数走缓存和异步累加,短暂不准完全可接受。这比把整个系统强行压到某一个极端模型要合理得多。设计时唯一要注意的是:每个数据维度的一致性级别要写在设计文档里,否则后面接手的人会把 AP 的缓存直接当成事实数据去读,出问题后互相甩锅。
6. 你的系统里,CP 还是 AP?这是我平时套用的决策框架
6.1 三个维度先给方向:业务后果、数据形态、恢复窗口
判断一个具体业务该偏 CP 还是偏 AP,我看三件事:数据冲突后的业务损失、字段本身是否可合并、业务能不能容忍一个恢复窗口。整理成表就是:
| 判断维度 | 偏向 CP | 偏向 AP |
|---|---|---|
| 并发冲突后果 | 双写会造成资金损失、状态错乱 | 冲突可以合并或覆盖 |
| 数据形态 | 余额、库存、订单、状态机 | 计数器、集合、日志、画像 |
| 读取期待 | 读自己写、强一致、单值准确 | 可接受短暂旧值,用户无感 |
| 不可用代价 | 短暂拒绝可以接受 | 拒绝请求会造成更大损失 |
比如余额就是典型的 CP,你不能用 LWW 决定两笔转账谁覆盖谁;购物车集合就可以用 OR-Set 做 AP,两个终端加的商品最后并集就行。
6.2 决策清单七问,帮你把模糊感觉变成明确结论
- 第一问:这条数据的并发写入冲突,业务上能合并吗?能合并,AP 有戏;不能,考虑 CP。
- 第二问:读取者需要马上看到最新值吗?需要,选 CP 或会话一致性;不需要,AP 更灵活。
- 第三问:分区发生时的“不可用窗口”,用户能接受吗?不能接受,AP;能接受,CP 更安全。
- 第四问:分区结束后,你能用任务把两边的数据对清楚吗?能对清,AP 后患可控;对不清,别碰 AP。
- 第五问:数据丢失的代价大吗?如果丢了只能靠人工补,选 CP 或至少 NWR 高 W。
- 第六问:系统是不是有选主、锁、配置同步这类需求?这类需求独立出来走 CP,别混进 AP 注册中心。
- 第七问:团队有没有线上演练和对账工具?没有运营能力的团队,别轻易上高强度 AP。
这套问题问完,大多数场景的方向就非常清楚了。剩下的极少数模糊地带,我建议按“宁可牺牲一点可用性,也不要让数据错”来兜底,因为数据错在线上是事故,可用性降低最多是性能问题,两件事的处理成本完全不同。
6.3 最后提醒一个高频翻车点:分区恢复不等于数据恢复
很多架构师会在设计评审时把 CP/AP 讲得头头是道,但真正落实到故障处理时,忽略了一个关键点:网络分区结束之后,系统并不是自动回到一致状态。分区期间两侧都在写,恢复之后需要触发补偿、对账、合并,这个过程本身又有延迟。如果业务在恢复后的第一分钟就开始读数据,读到的很可能还是分裂状态下的残留。
所以每次故障演练,我不只看系统在分区中的表现,更看恢复后的收敛速度和业务补偿是否自动触发。一个 AP 系统设计了自动对账但没人维护对账任务,和没设计一样;一个 CP 系统分区时正确拒绝了写,但恢复后没有检查哪批写没成功,也会造成数据缺口。
把这些边界想清楚,CAP 理论就不再是 PPT 里的概念,而是你真正常用的架构尺子。选 CP 还是 AP,从来不是技术面子问题,而是你愿意为哪类风险买单的问题。