Skip to content

CAP、BASE 与一致性理论

CAP 和 BASE 是理解分布式系统的理论基础。它们不是为了背面试题,而是帮助你判断:为什么有些系统宁愿短暂读到旧数据,也要保证服务可用;为什么有些资金系统宁愿拒绝请求,也不能返回不确定结果。

CAP 是什么

CAP 指三个目标:

字母含义初学者理解
CConsistency 一致性所有节点看到的数据一样,读到最新写入
AAvailability 可用性每个请求都能在有限时间内得到非错误响应
PPartition Tolerance 分区容错性节点之间网络断开时,系统仍然能处理问题

最容易误解的是“CAP 三选二”。更准确的说法是:

在分布式系统里,网络分区必须考虑;当分区真的发生时,系统只能在一致性 C 和可用性 A 之间取舍。

P 不是你想不想要的问题。只要系统跨机器、跨网络,就必须面对网络分区。

C 不是“所有数据永远一样”

CAP 里的 C 更接近“线性一致性”:一次写入成功后,后续读请求应该读到这次写入的结果,系统表现得像只有一个最新副本。

小白容易把一致性理解成“数据库里最后一定一样”。这还不够。真正难点在于:

  1. 写请求已经返回成功了,另一个节点还能不能读到旧值。
  2. 网络断开时,两个分区能不能同时接受互相冲突的写。
  3. 请求超时时,调用方不知道下游到底有没有成功。
  4. 多副本异步复制时,读从库、读缓存、读 ES 可能读到历史版本。

所以 CAP 讨论的不是“最终会不会同步”,而是“分区发生时系统是否还允许用户看到或写入不确定状态”。

A 不是“永远不宕机”

CAP 里的 A 指每个到达非故障节点的请求都能在有限时间内得到响应,而且响应不是简单报错。

这和日常说的高可用不完全一样。比如系统发生网络分区时,如果少数派节点为了保护一致性拒绝写入,它在 CAP 语义下就是牺牲了 A;但从工程上看,这是有意保护数据正确性的降级。

P 不是“分库分表”

Partition 指网络分区:消息丢失、网络中断、机房隔离、路由异常、DNS 异常、防火墙策略错误、交换机故障,都可能让两个节点短时间互相不可达。

只要服务通过网络通信,就不能假设网络永远可靠。分布式系统所有“超时、重试、幂等、补偿、对账”的设计,根本原因都来自这个不可靠网络。

什么是网络分区

网络分区不是数据库分库分表,而是节点之间网络不通。

mermaid
flowchart TD
    A["用户请求"] --> B["服务节点 A"]
    A --> C["服务节点 B"]
    B --> D["数据库主节点"]
    C -. "网络中断" .-> D

此时节点 B 可能无法访问数据库主节点。如果它继续返回本地旧数据,可用性好但一致性差;如果它拒绝请求,一致性更安全但可用性下降。

分区发生时为什么不能同时要 C 和 A

假设库存服务有两个节点 A 和 B,网络断开后两个节点互相同步不了。

mermaid
flowchart TD
    A["库存副本 A<br/>库存=1"] --> B["用户 1 下单"]
    C["库存副本 B<br/>库存=1"] --> D["用户 2 下单"]
    A -. "网络断开,无法同步" .- C
    B --> E{"A 是否允许扣减"}
    D --> F{"B 是否允许扣减"}

如果两个副本都为了“可用”接受扣减,就可能把同一件库存卖给两个人,一致性被破坏。

如果为了“一致”,不能确认全局最新状态的副本就必须拒绝请求或等待多数派确认,可用性下降。

这就是 CAP 的核心:不是系统设计者不够努力,而是在网络分区这个前提下,信息无法传递,系统不可能凭空知道另一个分区发生了什么。

CP 和 AP 怎么选

CP:优先一致性

分区发生时,宁愿拒绝部分请求,也不返回可能错误的数据。

适合:

  1. 余额扣减。
  2. 支付记账。
  3. 库存最终确认。
  4. 权限变更。
mermaid
flowchart TD
    A["发生网络分区"] --> B{"是否能确认最新数据"}
    B -- "能" --> C["继续处理"]
    B -- "不能" --> D["拒绝请求或等待恢复"]
    D --> E["保护一致性"]

CP 不是“系统一定不可用”,而是“不能确认一致性时宁愿拒绝”。ZooKeeper、etcd 这类协调组件常用多数派提交:只有能拿到多数派的分区能继续写,少数派不能写。这样可以避免两个分区各自写出冲突结果。

代价是:

  1. 少数派节点看起来还活着,但不能对外提供完整写能力。
  2. 写请求要等多数派确认,延迟更高。
  3. 网络抖动时可能出现请求失败、重试和短暂不可用。

AP:优先可用性

分区发生时,允许短暂不一致,后续通过同步、补偿、对账修复。

适合:

  1. 商品详情缓存。
  2. 用户昵称展示。
  3. 浏览量、点赞数。
  4. 搜索索引同步。
  5. 消息通知。
mermaid
flowchart TD
    A["发生网络分区"] --> B["先返回可用结果"]
    B --> C["可能是旧数据"]
    C --> D["网络恢复后同步修正"]

AP 不是“数据错了也没关系”,而是“先保证服务可用,再通过版本、消息、补偿、对账把副本修正回来”。如果没有修复闭环,只是把错误藏起来,那不是 BASE,而是不可靠。

代价是:

  1. 用户可能短时间读到旧数据。
  2. 下游可能收到重复或乱序事件。
  3. 业务必须设计幂等、版本号、状态机、补偿任务。
  4. 监控必须能发现长期不一致,否则“最终一致”不会自动发生。

CP、AP 不是系统永久标签

很多系统不能简单贴一个 CP 或 AP 标签,因为不同操作可能选择不同策略。

系统或业务常见选择解释
ZooKeeper 写入CP写入需要多数派确认,少数派不可写
服务注册信息读取AP 倾向消费者通常缓存服务列表,短时间旧地址可接受
MySQL 主库写订单CP 倾向订单状态以主库事实源为准
MySQL 到 ES 同步AP/最终一致ES 是搜索视图,可以延迟和重建
Redis 缓存AP/最终一致缓存可短暂旧,事实源在数据库
支付渠道回调最终一致 + 对账回调可能丢失,最终以渠道账单和本地流水对账

面试时不要只说“某某组件是 CP/AP”,更好的回答是:这个组件在什么操作上偏 CP,什么场景下为了可用接受最终一致。

商业场景例子

场景更偏向原因
支付扣款CP不能重复扣款或少扣款
订单状态最终展示CP + 最终一致核心状态要可靠,展示可短暂延迟
商品搜索APES 可以短暂落后 MySQL
商品库存展示AP页面库存可短暂不准,下单时再强校验
秒杀库存扣减CP最终扣减不能超卖
用户头像AP短暂旧头像影响小

CAP 为什么影响分布式事务

分布式事务的本质是追求多个资源之间的一致性。越追求强一致,就越可能牺牲可用性和性能。

以 XA/2PC 为例:

mermaid
flowchart TD
    A["全局事务提交"] --> B["资源 A prepare"]
    A --> C["资源 B prepare"]
    B --> D{"所有资源 ready?"}
    C --> D
    D -- "是" --> E["统一 commit"]
    D -- "否或超时" --> F["统一 rollback 或等待恢复"]

当资源或协调者异常时,系统可能阻塞等待决议。这就是为了强一致付出的代价。

而可靠消息、本地消息表、Saga 更偏向 BASE 和最终一致:先让本地事务成功,再通过异步消息和补偿让其他服务最终达到一致。

BASE 是什么

BASE 是对强一致的一种工程化折中:

概念含义示例
Basically Available 基本可用系统出现故障时,保证核心能力可用大促时关闭非核心推荐
Soft State 软状态系统允许中间状态存在订单支付中、库存锁定中
Eventually Consistent 最终一致短时间不一致,最终通过同步修正MySQL 更新后 ES 稍后同步

BASE 不是“不保证一致性”,而是“不要求每一刻都强一致”。它要求系统最终能通过重试、补偿、对账恢复到正确状态。

BASE 三个词放到订单系统里理解

BASE 概念订单系统例子如果没有这个设计会怎样
基本可用大促时下单、支付保留,推荐、报表、非核心弹窗降级非核心功能拖垮核心链路
软状态订单存在 WAIT_PAYPAYINGSTOCK_LOCKEDSYNCING_ES只能成功/失败两态,无法表达中间过程,补偿困难
最终一致支付成功后订单状态、积分、通知、ES 通过消息逐步完成任一步失败都可能永久不一致

BASE 的重点是“允许中间状态”,所以商业项目必须把中间状态设计成可观察、可重试、可补偿,而不是只在内存里记一下。

最终一致不是放任不管

错误理解:

text
最终一致 = 反正最后差不多一致,失败也无所谓

正确理解:

text
最终一致 = 允许短暂不一致,但必须有机制保证最终能修正

最终一致需要:

  1. 消息可靠投递。
  2. 消费端幂等。
  3. 失败重试。
  4. 死信队列。
  5. 定时补偿。
  6. 对账任务。
  7. 告警和人工处理。

最终一致的完整闭环

mermaid
flowchart TD
    A["本地事务提交事实源"] --> B["记录事件或消息"]
    B --> C["投递 MQ 或 CDC 捕获"]
    C --> D["下游幂等消费"]
    D --> E{"处理成功?"}
    E -- "是" --> F["记录成功状态"]
    E -- "否" --> G["重试"]
    G --> H{"超过阈值?"}
    H -- "否" --> D
    H -- "是" --> I["死信或异常表"]
    I --> J["补偿任务 / 对账 / 人工处理"]

少了任何一环都会出问题:

缺少环节典型后果
没有事件记录本地事务成功但消息丢失,下游永远不知道
没有幂等重试或重复消费导致重复加积分、重复扣库存
没有死信问题消息无限重试,拖慢整个消费组
没有补偿暂时失败变成永久不一致
没有对账未知失败无法发现,只能等用户投诉
没有告警异常积压很久没人处理

PACELC 补充理解

PACELC 是对 CAP 的补充:

如果发生分区 P,在可用性 A 和一致性 C 之间取舍;否则 E,在延迟 L 和一致性 C 之间取舍。

也就是说,即使没有网络分区,系统也经常要在低延迟和强一致之间权衡。

例子:

选择结果
每次读都查主库一致性更好,但延迟可能更高
读缓存或读从库延迟更低,但可能读到旧数据
写入同步复制多个节点数据更可靠,但写入更慢
写入一个节点后异步复制写入快,但短暂不一致

一致性级别

一致性说明示例
强一致写成功后任何读都能读到最新值余额
顺序一致所有节点看到的操作顺序一致协调服务
单调读一个用户读到新值后不会再读到旧值用户订单状态
读己之写自己写完后自己能立刻读到修改昵称后自己看到新昵称
最终一致经过一段时间后各节点一致搜索索引、缓存

不是所有业务都需要强一致。小白最容易犯的错是把所有一致性问题都当成强一致处理,结果系统复杂、慢、难维护。

常见一致性模型怎么落地

一致性模型商业落地做法适合场景
强一致只读写主库、单库事务、数据库唯一约束、条件更新余额、支付流水、订单核心状态
读己之写写入后本人短时间读主库,或者把新值写入会话缓存用户资料、商家后台编辑商品
单调读同一用户固定读同一副本,或读请求携带版本号订单详情页刷新不能从已支付跳回待支付
因果一致事件带上前置版本或业务状态,消费端按状态机处理先创建订单,再同步订单明细,再同步 ES
最终一致MQ、CDC、本地消息表、补偿、对账搜索、积分、通知、报表、缓存

状态机为什么是一致性工具

状态机的作用是限制数据只能按合法路径变化。

mermaid
flowchart TD
    A["WAIT_PAY<br/>待支付"] --> B["PAID<br/>已支付"]
    A --> C["CLOSED<br/>已关闭"]
    B --> D["DELIVERING<br/>履约中"]
    D --> E["FINISHED<br/>完成"]
    B --> F["REFUNDING<br/>退款中"]
    F --> G["REFUNDED<br/>已退款"]

如果没有状态机,重复回调、乱序消息、补偿任务可能把订单从 PAID 改回 WAIT_PAY,或者把已经关闭的订单改成已支付。正确做法是每次更新都带当前状态条件:

sql
update t_order
set status = 'PAID',
    paid_at = now()
where order_no = ?
  and status = 'WAIT_PAY';

更新行数为 1 表示状态流转成功;更新行数为 0 表示订单已经被其他流程处理过,本次请求应该走幂等返回或查询确认。

代码 Demo:读己之写

用户修改资料后,自己应该立刻看到新资料,但其他用户稍后看到也可以接受。

一种做法是:修改后短时间内对本人读主库,其他场景读缓存。

java
public UserProfile getProfile(Long currentUserId, Long targetUserId) {
    if (currentUserId.equals(targetUserId)) {
        return userRepository.findById(targetUserId);
    }

    UserProfile cached = userProfileCache.get(targetUserId);
    if (cached != null) {
        return cached;
    }

    UserProfile profile = userRepository.findById(targetUserId);
    userProfileCache.put(targetUserId, profile);
    return profile;
}

这个例子说明:一致性不一定只有“全局强一致”一种做法,可以按用户体验和业务风险设计局部强一致。

CAP 与技术选型

技术更偏向说明
XA/2PCCP强一致,可能阻塞
TCCCP 倾向业务层资源预留,强控制
SagaBASE本地事务 + 补偿
本地消息表BASE异步消息最终一致
MQ 事务消息BASE保证业务提交和消息发送一致
Redis 缓存AP 倾向允许短暂旧值
ES 搜索索引AP 倾向MySQL 更新后异步同步

本章小结

CAP 告诉你:分布式系统遇到网络分区时,不可能同时完美保证一致性和可用性。BASE 告诉你:很多商业系统可以接受短暂不一致,但必须通过重试、补偿、对账保证最终修正。

理解 CAP/BASE 之后,再看分布式事务就会清楚:XA/TCC 是为了更强一致付出更高成本;本地消息表、事务消息、Saga 是为了更高可用和吞吐接受最终一致。