CAP、PACELC 与 BASE:别把取舍模型当成系统标签
从一次跨机房库存写入出发,区分 CAP 的分区语义、PACELC 的日常延迟取舍,以及 BASE 在业务设计中的实际责任。
“这个数据库是 AP 还是 CP?”“用了 BASE,是不是就不需要事务?”这两类问法都过早把结论贴到了系统身上。CAP、PACELC 和 BASE 的作用不同:CAP 给出网络分区下无法回避的约束;PACELC 把正常运行时的延迟代价放回讨论;BASE 则是一组面向业务实现的宽松原则。它们都不能替你定义一笔操作何时算成功。
本文用一个跨机房库存服务回答一个具体问题:当两个副本暂时失去通信时,系统该拒绝请求、返回旧值,还是允许继续写入;网络正常时,又该用多少等待时间换取多强的读取语义。术语和定理主要依据 Gilbert 与 Lynch 的 CAP 证明、Abadi 对 PACELC 的论述,BASE 的历史来源则参照 Pritchett 的文章记录。例子是合成系统,具体产品的默认行为必须以其版本、读写参数和部署方式为准。
先把三者放回各自的位置
| 名词 | 它回答的问题 | 适用时刻 | 不能替你回答的问题 |
|---|---|---|---|
| CAP | 网络分区中,强一致和每个非故障节点都响应能否同时满足 | 节点间消息可能丢失或无限延迟时 | 正常时延、事务隔离、冲突怎样合并 |
| PACELC | 分区时选 A 还是 C;未分区时用 L 还是 C | 故障与正常运行都覆盖 | 某产品实际承诺的精确一致性级别 |
| BASE | 哪些业务状态可以容忍暂时不一致并异步收敛 | 面对可用性与扩展压力时 | 不变量、补偿流程和可接受陈旧度 |
读这三个词有一个顺序。先写出业务不变量,例如“同一件限量商品不能卖给两个人”“支付不能重复扣款”;再确定出现结果未知、网络隔离或副本落后时接口要怎样表现;最后选择数据库、复制协议和读写参数。直接从“我们要 AP”开始,容易把系统的正确性责任交给一个缩写。
图里最容易被忽略的是右侧的 Else。系统绝大多数请求在副本仍可通信时发生,用户仍会感受到一次写入要不要等多数副本、一次读取能不能就近返回。这部分不由 CAP 定理决定。
一、先定义例子:跨机房的库存副本
电商有两个机房,机房 A 和 B 都保存 sku-42 的可售库存。初始库存为 1。用户下单时,库存服务提供两个接口:
reserve(orderId, sku, quantity) # 预占库存
getAvailable(sku) # 查询可售库存
如果这套接口允许“同一个 orderId 重试仍只预占一次”,它还需要幂等记录;如果订单取消,库存还需要释放。这里先把焦点缩小为一条更基础的不变量:任意时刻,成功预占的总数不能超过 1。
正常情况下,A 收到预占请求后将写入复制给 B。一个强一些的做法是在确认足够副本持久化之前不向客户端返回成功;一个快一些的做法是 A 本地写完就返回,稍后异步复制。两种做法都可能合理,但它们给调用方的“成功”含义不同。
现在 A 与 B 之间出现网络分区。机房内的应用和数据库都还活着,只有跨机房通信断开。A 的用户请求 reserve(o-1, sku-42, 1);B 的用户请求 reserve(o-2, sku-42, 1)。此时系统没有一个无需额外假设的办法,同时证明两笔预占都安全,又让两边都立即成功。
这不是机器真的停了才会出现的问题。最危险的情形恰恰是每一侧看起来都很健康:CPU 正常、连接到本地副本正常、探针也通过,但对侧的状态已经不可见。
二、CAP 讨论的是分区期间的一道硬约束
CAP 的 C、A、P 与日常口语里的“数据一致”“高可用”“支持多机房”并不完全一样。Gilbert 与 Lynch 的形式化定义把 C 表示为 atomic consistency,实践中可以近似按单对象的线性一致理解:一个已经完成的写入,之后开始的读取不能再读到旧值。A 要求每个送达非故障节点的请求最终都得到响应;P 允许节点间消息被丢失或无限延迟。
把 A、B 之间的网络完全切断。若 A 接受 o-1 并返回成功,B 面对 o-2 时看不到 A 的决定。B 只有三类选择:
- 也返回成功:两个机房各自出售了唯一库存,违反库存不变量;
- 拒绝或无限等待:B 的请求没有得到成功响应,牺牲了 CAP 意义上的可用性;
- 返回“暂时不能确认”:接口仍然没有把这次预占当作可用的成功路径,业务上需要重试、排队或降级。
因此,常见的“C、A、P 三选二”并不精确。对一个要容忍网络分区的分布式系统,P 不是一个可随手放弃的功能;真正需要在分区期间决定的是,对当前操作优先保持 C,还是让每个可达副本继续给出响应。CAP 也没有说整个系统永远固定为 CP 或 AP。一个系统可以让库存预占在分区时拒绝,而让商品详情继续从本地副本读取。分类应该落在数据项、操作和承诺级别。
CP:不能证明安全时,宁可不确认
库存服务若要守住“最多卖一件”,可以要求写入必须落到多数派,或者由唯一主副本裁决。当分区把两节点恰好分成一边一个时,任何一侧都无法形成多数派,于是 reserve 返回不可用、进入排队,或提示稍后重试。
对用户来说这是一次失败或延迟;对系统来说,它避免把没有证据的成功写进订单链路。CP 的价值不在于“数据永远不丢”,而在于接口不会在分区里确认相互矛盾的决定。恢复通信后,日志或共识协议可以继续从同一条决定历史推进。
CP 也不等于所有读都必须慢。某些系统会为读取提供租约、ReadIndex 或多数派读;有些业务接受从副本读旧值。关键在于接口要把语义写清楚:getAvailable 返回的是可用于立即下单的权威库存,还是可能落后的展示数字。前者通常需要更强的协调,后者可以单独放宽。
AP:先让本地继续工作,再处理分叉
如果业务允许两边都接受更新,系统选择的是另一条路。例如用户给商品点收藏、增加浏览计数、保存草稿,分区时可以由 A 和 B 分别接收操作,网络恢复后再合并。
这里的前提不是“以后一定会神奇地一致”,而是合并规则已经存在。计数可以按各副本增量相加;收藏可以使用带唯一操作标识的集合;同一字段同时修改时,可能需要 Last Write Wins、版本向量、人工处理或业务优先级。合并规则决定最终状态,也决定哪些用户操作可能被覆盖。
把库存预占改成 AP 并不会自动得到可用库存。两边都成功后再发现只有一件货,系统仍要取消其中一单、补偿优惠券、解释支付状态。能不能承担这些后果,才是 AP 的业务判断。对于唯一所有权、余额扣减、配额上限等状态,冲突往往不是简单的字段合并问题,而是已经违反了不可逆的不变量。
三、CAP 的边界:它没有规定“平时应该多快”
网络没有分区时,A 和 B 可以互通,但跨机房往返仍有耗时。为了让一次 reserve 在返回时已被两个副本确认,A 至少要等待复制和确认。为了让读取在写入完成后总能看到新值,读取也要找权威副本或进行协调。用户会在每一笔请求上支付这个等待时间。
另一种设计把写入先记录在本地、读取就近返回,把复制交给后台。这样尾延迟通常更低,但刚写完的用户可能在另一个机房读到旧值;两个并发写入也可能需要后续合并。网络完全健康时,这种取舍仍然存在,CAP 没有替你选择。
还要区分“没有被监控判定为分区”和“没有延迟”。超时阈值以前,节点无法可靠分辨对方是慢、拥塞、GC 停顿,还是已经不可达。把阈值调大可以少误判,却让请求等待更久;调小能更快切换,却更容易把慢副本当成故障。工程里的可用性和延迟预算因此总是连在一起。
这也是为什么不能拿 CAP 给一次常规的性能优化背书。页面展示库存从异步副本读取,理由可能是 P99 延迟;支付确认写入多数派,理由可能是账务不变量。它们都可以合理,却不是“因为 CAP 规定必须这样”。
四、PACELC 把正常路径也纳入提问
Abadi 提出的 PACELC 写法是:发生 Partition 时,在 Availability 和 Consistency 间取舍;Else,在 Latency 和 Consistency 间取舍。它是帮助设计者审视复制系统的框架,并不是对所有产品都能用一个四字母标签完整描述的规格。
把前面的库存服务代入,问题会更具体:
| 场景 | 需要问的问题 | 库存预占的可能选择 | 商品展示的可能选择 |
|---|---|---|---|
| A、B 无法通信 | 还要不要在本地确认写入 | 否,拒绝或等待多数派 | 可以,返回本地缓存或副本值 |
| A、B 可通信但跨机房慢 | 成功响应要不要等远端确认 | 要,接受更高写入延迟 | 未必,允许数秒陈旧 |
| 刚写完立刻读 | 读是否必须看见自己的写 | 是,走主副本或会话令牌 | 通常不必,读最近副本 |
因此,说一个系统“PC/EC”或“PA/EL”最多是一张粗略地图:前半段描述分区时的倾向,后半段描述正常时更偏向一致性还是延迟。实际服务通常有可调的读写级别、不同的数据表和不同的操作路径。甚至同一张表,管理员修改库存上限与用户读取列表也会得到不同答案。
PACELC 的实际价值在于强迫团队同时写下两张表:故障时接口如何退化,正常时每个确认点花多少时间。只有前者会得到一套“灾难发生时才有意义”的架构;只有后者则可能在真正分区时做出未经演练的选择。
把延迟换成可观察的承诺
不要只在设计文档里写“强一致读”或“最终一致”。可以把承诺拆成调用方可验证的语句:
reserve(orderId, sku, quantity)
成功:预占已写入多数派;同 orderId 的相同请求返回同一结果
超时:结果未知,客户端必须用 orderId 查询,不得换新键直接重放
getReservation(orderId)
强查询:在成功预占返回后,必须读到 CONFIRMED 或后续终态
展示查询:允许读到不超过 3 秒的副本滞后值,响应含 asOfVersion
这里的“多数派”“3 秒”“结果未知”都可以被测试、监控和告警。它们也提醒前端和 Agent:超时不是失败的同义词,展示数据与交易状态不能共享同一个隐含假设。
五、BASE 是实现风格,不是放弃正确性
BASE 通常被展开为 Basically Available、Soft state、Eventually consistent。它出现在互联网服务尝试用复制、异步处理和分区来扩展时,故意与 ACID 形成对照。这个名字很容易让人误以为:只要能最终收敛,就可以不再讨论事务、约束和失败。
更有用的理解是,BASE 把一部分协调从同步请求路径移到异步过程。系统优先维持部分可用,允许副本状态在没有新用户写入时仍因复制或合并而变化,并期待在没有持续新更新的前提下收敛。每一个词都有条件。
Basically Available 不等于每次操作都成功
系统可以让读详情、查看历史订单、保存本地草稿继续可用,同时让涉及库存所有权的动作排队或被拒绝。把“系统的一部分还可用”写成“所有接口都成功”,会让调用方无法处理真正的风险。
可用的降级结果需要有业务语义。例如返回 PENDING_CONFIRMATION 表示订单还没有获得库存,不应发货;返回缓存商品页可以标记更新时间;付款渠道不可达时保存支付意图但不扣账。好的降级会缩小承诺,而不是伪造成功。
Soft State 必须有来源、版本和终点
副本状态会变是正常的:A 的预占日志复制到 B,B 的展示库存从 1 变成 0,即便 B 没收到新的本地请求。但这不代表状态可以随意漂移。每个异步状态至少要有来源事件、版本或幂等标识,以及一个能观察的终态。
例如订单服务收到 InventoryReserved(orderId, version=18) 后更新订单状态;重复投递同一事件不能让状态倒退,版本缺口应被记录,长时间停在 PENDING_STOCK 的订单要能被扫描和补偿。没有这些机制,Soft State 只是“系统会自己变”的另一种说法。
Eventually Consistent 需要说明“最终”是什么条件
最终一致的定义包含一个常被省略的前提:如果不再有新的更新,副本最终会收敛。现实中的热卖库存、持续写入的点赞数和不断变更的用户资料不满足这个静止条件,因此更值得问的是:副本最大可能滞后多久,读到旧值会造成什么,冲突由谁解决。
对商品详情,短暂读旧价格也许只需在结算时重新校验;对余额,旧值可能诱导重复消费;对权限撤销,旧副本继续放行可能构成安全问题。BASE 不负责判断这些风险,它要求业务把“可接受的不一致”具体化。
六、从模型回到一次下单:状态怎样收敛
把库存预占放进完整下单流程,问题会比“读到旧值”清晰得多。订单服务先创建 PENDING_STOCK 订单,带上全局唯一的 orderId;库存服务尝试预占;只有明确得到 CONFIRMED 才进入待支付;超过时限则走查询和释放流程。
POST /orders (orderId=o-1)
→ 订单:PENDING_STOCK
→ 库存:reserve(o-1, sku-42, 1)
├─ CONFIRMED → 订单:PENDING_PAYMENT
├─ REJECTED → 订单:OUT_OF_STOCK
└─ UNKNOWN → 订单:PENDING_CONFIRMATION
↓ 查询预占记录 / 对账
CONFIRMED 或 RELEASED
若库存采用 CP 路线,分区里 reserve 更可能返回 REJECTED 或暂不可用,订单可以提示用户稍后再试。若某个非关键环节采用 AP 路线,例如订单时间线的展示副本,它可以晚一点同步;但它不能替交易主状态宣布下单成功。
若库存的设计允许异步预占,那么补偿不应藏在一段 catch 代码里。它需要持久化任务、重试上限、死信或人工处理入口,以及可关联 orderId 的审计记录。一次消息延迟或重复投递不会因为“BASE”而变得安全,只有幂等状态机和可验证的补偿让它可恢复。
这个例子也说明了 ACID 与 BASE 不是二选一。库存服务内部可以用本地事务原子写入预占记录和 Outbox;跨服务通过异步事件收敛;支付账本仍可能要求严格事务。系统不同部位需要不同保证,组合比标签更重要。
七、把取舍落在数据类型,而不是整个系统
一个订单系统很少需要把全部数据放进同一种一致性模型。把“订单系统是 CP”或“我们的架构是 BASE”写在总览图上,对实现几乎没有帮助。更可执行的做法是先按数据会造成的后果划分,再为读写路径写承诺。
| 数据或操作 | 失去实时一致性会发生什么 | 常见策略 | 需要额外写明的内容 |
|---|---|---|---|
| 库存配额、唯一用户名、租约所有者 | 同一资源被重复占用 | 同步协调、条件写、多数派确认 | 拒绝条件、超时后查询方式 |
| 支付账本、退款状态 | 重复扣款或账实不符 | 本地事务加幂等键、可审计状态机 | 业务键、对账与人工处理 |
| 商品详情、搜索索引、推荐列表 | 用户短暂看到旧内容 | 异步复制、缓存失效、重建索引 | 最大陈旧度、刷新入口 |
| 点赞、浏览数、收藏集合 | 计数暂时偏差或重复展示 | 去重操作、可交换的数据结构、异步合并 | 操作 ID、冲突合并规则 |
| 权限撤销、风控封禁 | 已撤销用户仍能访问 | 权威路径同步校验,缓存短 TTL 或主动失效 | 撤销传播 SLO、失败时的默认策略 |
第一行的特点是存在非此即彼的不变量。库存为零后不能再成功预占,用户名已被占用后不能再分给另一个账户。这里常要用协调来保护决定本身,但协调未必意味着所有展示读都走同一条慢路径。例如结算页在提交前再向权威库存确认一次,商品列表仍可显示稍旧的“仅剩 1 件”。
第二行常被误称为“强一致数据库的问题”,其实更接近动作去重和账务可追溯的问题。支付渠道已经受理、但客户端收不到响应时,就算底层数据库线性一致,调用方仍不知道该不该重试。业务幂等键、渠道流水号和对账流程构成另一层事实来源。CAP 不能替这层机制作保证。
第三、四行更适合讨论 BASE 式的异步收敛,但也要避免把所有字段都交给 Last Write Wins。搜索索引可以从主数据重建,因而允许暂时落后;收藏则需要知道同一操作是否已处理过;计数可能接受近似值。不同的可合并性决定了不同的复制和修复方式。
权限和风控是一个有用的反例。它们看上去像“读取频繁、更新少”的缓存场景,似乎适合异步副本;但撤销读旧的后果可能比暂时拒绝一个合法用户更严重。此时团队必须明确 fail-open 还是 fail-closed:缓存服务不可用时默认放行,还是默认要求重新校验。这个选择是安全与可用性的业务取舍,不能用 AP 或 CP 两个字遮住。
用 SLO 让“最终”可以被追责
一旦选择异步路径,就应把收敛当成一项运行目标,而非后台任务的乐观愿望。可以为每一类投影或副本记录三个量:事件从主记录发出到被消费的延迟、尚未处理事件的积压量、同一业务键在两处状态不一致的数量。前两个反映管道健康,第三个直接反映业务结果。
例如,商品搜索索引可以约定“99% 的更新在 30 秒内可查询”,并在积压超过阈值时暂时从主库检索或提示数据正在更新。订单与库存若超过五分钟仍分别处于 PENDING_CONFIRMATION 和未知状态,则自动进入对账队列并创建人工工单。这里的数字需要从业务时效和实际链路测量得来,不能从 CAP 结论推导出来。
这类指标也能防止一个常见误区:系统恢复通信就代表已经恢复正确。网络恢复只让同步成为可能;积压消息、重复投递、失败的补偿和已经被用户看见的旧状态还要继续处理。恢复演练应覆盖分区结束后的收敛速度和悬挂状态数量。
八、常见误用,以及更准确的问法
“CAP 三选二,所以可以选 CA”
单机数据库没有跨节点网络分区的场景,不是 CAP 意义上“解决了 P”。真正跨网络复制的系统必须面对通信断开,分区发生时要说明谁停、谁继续、谁承担冲突。与其问“是不是 CA”,不如问“网络隔离后,哪一侧还能确认哪些操作”。
“CP 一定不可用,AP 一定高可用”
CAP 的 A 是严格定义:每个到达非故障节点的请求最终响应。生产里的高可用还包括容量、依赖故障、发布、限流和恢复时间。CP 系统在多数派健康时可以提供很好的可用性;AP 系统也可能因为本地磁盘、限额或下游不可用而拒绝请求。CP、AP 只描述特定分区下的优先级。
“最终一致就是稍后读一遍”
读取重试可能让用户碰巧看到新数据,却没有定义收敛条件、冲突规则和超时后的处理。需要明确的是:写入是否带版本,副本怎样同步,多久未收敛算异常,读到旧值的操作能否继续,以及人工如何修复悬挂状态。
“选了一个数据库,就选定了全系统的 CAP 属性”
数据库的复制模式只是其中一层。网关缓存、搜索索引、消息队列、订单状态机和第三方支付各自有不同的读写语义。产品也常通过 quorum、读偏好和一致性级别提供多种路径。架构评审应画出关键操作的数据路径,逐个标记确认点和降级行为。
九、评审时可以留下的八个问题
在设计评审或接口评审中,下面的问题比“这是 AP 还是 CP”更能暴露缺口:
- 这个操作必须保护的业务不变量是什么?
- 客户端收到成功时,哪些副本或持久化记录已经确认?
- 超时后,调用方如何区分未执行、已执行和执行中?
- 网络隔离时,哪些接口拒绝、排队、返回旧值或继续接受写入?
- 正常运行时,为了更强读写语义多等了哪一跳,预算是多少?
- 哪些数据允许陈旧,陈旧值会被谁消费,最长能持续多久?
- 并发冲突的合并规则是什么,谁有权覆盖谁?
- 状态没有收敛时,有没有指标、对账任务和人工接管路径?
如果这些问题都有答案,CAP、PACELC 和 BASE 就完成了它们的工作:它们帮助团队暴露取舍,而没有代替业务作出取舍。
参考资料
- Seth Gilbert, Nancy Lynch, Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services
- Daniel J. Abadi, Consistency Tradeoffs in Modern Distributed Database System Design: CAP is Only Part of the Story
- Dan Pritchett, BASE: An ACID Alternative
- Eric Brewer, CAP Twelve Years Later: How the “Rules” Have Changed
如果这篇文章对你有帮助