一致性取舍:CAP 与 BASE —— 分布式系统的第一道选择题
任何分布式系统的第一个设计决策都是:分区/故障发生时,保一致还是保可用? 本篇把 CAP 与 BASE 讲到能落地:精确定义、证明、误区、选型决策,以及最终一致性的工程实现模式。
一、CAP:三个性质,两个只能留一个
1.1 精确定义
| 性质 | 定义 | 注意 |
|---|---|---|
| C(Consistency) | 线性一致性:系统对外表现为单机,读返回最近一次写 | 是线性一致,不是最终一致 |
| A(Availability) | 每个请求在有限时间内返回非错误响应 | 不要求返回最新值,只要求"有响应且非错" |
| P(Partition tolerance) | 网络分区(消息丢失/节点失联)时系统继续按 C 或 A 提供服务 | 分区是必然事件,P 不可选 |
1.2 证明思路(一页纸)
设分区把节点分为 G1、G2,两组无法通信:
- 客户端写
x=1到 G1,成功。 - 客户端向 G2 读
x:- 保 C → G2 必须返回
1,但做不到 → 只能拒绝/挂起 → 牺牲 A; - 保 A → G2 必须响应 → 只能返回旧值 → 与 G1 不一致 → 牺牲 C。
- 保 C → G2 必须返回
结论:分区存在时 C、A 不可兼得。而分区无法避免,所以真实系统只有 CP 或 AP 两种路线。
1.3 四个高频误区(面试直接背)
- "三选二随便选"是错的 —— P 是必选,不存在"CA"选项(CA 只成立在"假设永不分区"的单机语义下)。
- C 不是最终一致性 —— 最终一致性属于 BASE,CAP 框架不讨论它。
- CAP 只约束分区时刻 —— 无分区时 CP 系统(如 etcd)读写照常;不能用 CAP 解释日常行为。
- 粒度是数据/模块 —— 一个系统可以混合:核心数据 CP、非核心数据 AP。
1.4 PACELC:补上"无分区时"的权衡
CAP 只回答分区时怎么选,PACELC 补全无分区时的选择:
Partition(分区时):Availability vs Consistency Else(无分区时):Latency vs Consistency
典型:多数派强一致系统(etcd)无分区时每次写也要等多数派 fsync——用延迟换一致;异步主从(MySQL 异步复制)写只需主库确认——用一致换低延迟。
二、选型决策:CP 还是 AP
| 场景 | 选型 | 理由 |
|---|---|---|
| 分布式锁、选主、配置下发 | CP(etcd/ZooKeeper) | 锁和配置必须精确,宁可拒绝也不能给错 |
| 服务注册发现 | AP(Eureka) | 读到过期实例列表可接受;注册中心整体不可用才是灾难 |
| 强一致业务存储 | CP(TiDB、HBase、MongoDB 默认) | 金融/订单等场景数据不能丢不能错 |
| 缓存、计数、Feed | AP(Redis 异步复制、Cassandra) | 容忍短暂不一致,换取低延迟高可用 |
| 消息队列 | 依语义:Kafka 未确认 ack=0/1 偏 AP;事务性消息用 ISR 多数派偏 CP | 看业务是否允许消息丢失 |
注册中心选型的完整追问链:
- 为什么 Eureka 选 AP?→ 服务发现读的是"哪个实例活着",读到已下线实例最多是请求失败重试,可容忍;而注册中心宕机导致所有服务找不到彼此 = 全系统瘫痪,不可容忍。→ 所以保可用,牺牲"读到的一定最新"。
- 那 etcd 为什么被 K8s 用?→ etcd 存的是集群状态和配置,读错会导致调度错误,必须强一致;且 etcd 是少数派才不可用,多数派侧依旧服务。
三、BASE:AP 路线的工程化
3.1 三要素
| 要素 | 含义 | 落地表现 |
|---|---|---|
| 基本可用(Basically Available) | 故障时整体仍可用,允许降级 | 响应变慢、非核心功能关闭(降级)、限流削峰 |
| 软状态(Soft state) | 允许中间不一致状态存在 | 副本间数据短暂不同步是正常态 |
| 最终一致(Eventually consistent) | 无新写入后,副本最终收敛 | 靠异步传播 + 补偿收敛 |
3.2 与 ACID 的对比(背表)
| 维度 | ACID | BASE |
|---|---|---|
| 一致性 | 强一致,提交即生效 | 最终一致,靠收敛 |
| 可用性 | 一致性优先,故障可拒绝 | 可用性优先,短暂不一致可接受 |
| 模型 | 悲观(先锁后做) | 乐观(先做后补) |
| 典型 | MySQL InnoDB 事务 | 缓存、MQ、异步复制 |
四、最终一致性的工程实现模式(考点:必须能说出机制)
最终一致不是"放任不管",面试要能给出收敛机制:
4.1 异步复制 + 消息重试
写主库 → 同步发本地消息 → MQ投递 → 消费端同步到从库/下游
└ 失败进死信队列 → 重试 → 达到上限告警人工介入4.2 本地消息表 + 事务消息(解决"业务成功但消息没发出")
text
① 开启本地事务:写业务表 + 写消息表(同库同事务)→ 提交
② 后台任务扫描消息表 status=pending → 投递 MQ
③ 消费成功后更新消息表 status=done
④ 超过阈值未 done → 重投 / 告警保证:业务与消息同生共死,消息至少投递一次;配合消费端幂等做到"不重不丢"。
4.3 对账与定时补偿
定时任务对比两侧数据(如订单表 vs 支付流水),发现差异 → 补发/回滚/告警。对账是最终一致的最后兜底,任何异步链路都要有。
4.4 重试必须配幂等
异步同步必然重试,重试必然重复执行 → 唯一键 / 状态机 / token 幂等是铁律(与微服务篇一致)。
4.5 分布式事务模式(最终一致的强一致补充)
| 模式 | 机制 | 一致性 |
|---|---|---|
| TCC | Try 预留 → Confirm 提交 / Cancel 取消 | 最终一致(强于纯异步) |
| Saga | 正向事务链 + 反向补偿事务 | 最终一致 |
| 两阶段提交(2PC) | 准备 + 提交,协调者阻塞 | 强一致(有协调者单点、阻塞问题,少用) |
五、设计决策框架(面试怎么答)
回答"你这个系统 CP 还是 AP"的完整结构:
- 按数据分类:列出系统中的数据,标注一致性要求(余额/库存/订单=强;计数/Feed/缓存=弱)。
- 按组件选型:强一致数据用 etcd/MySQL 主从同步/分布式事务;弱一致用异步复制 + MQ。
- 说明降级路径:AP 部分分区时怎么降级(读旧缓存、关闭非核心);CP 部分分区时怎么处理(多数派继续,少数派拒绝)。
- 给收敛机制:MQ 重试、幂等、对账。
面试追问
- 问:Redis Cluster 是 CP 还是 AP? 取决于复制模式:异步复制下主挂切换可能丢最近写入 = AP;同步复制(wait 命令/红锁类)可近似 CP。缓存场景默认按 AP 容忍丢失设计。
- 问:Nacos 为什么有 CP 和 AP 两种模式? 服务发现场景用 AP(可用优先),配置中心用 CP(配置必须一致),一个组件两种模式对应两种数据的一致性要求不同。
- 问:能不能既保强一致又保高可用? 单机强一致可以,分布式下必须在分区时二选一;可用"多数派 + 降低少数派影响"缓解(CP 下多数派侧依然可用),但不能同时满足 CAP 定义下的 C+A+P。