[L2] CAP定理:一致性、可用性与分区容忍的三角权衡
一句话结论
网络分区不可避免,分布式系统只能在一致性与可用性之间二选一。
体系讲解
1. 三个属性的含义
| 属性 | 英文 | 含义 |
|---|---|---|
| 一致性 | Consistency | 所有节点在同一时刻看到相同数据(线性一致性) |
| 可用性 | Availability | 每个请求都能收到非错误响应(不保证数据最新) |
| 分区容忍 | Partition Tolerance | 节点间网络故障时系统仍能继续运行 |
2. 为什么只能选两个
CAP 定理(Brewer 2000,Gilbert & Lynch 2002 形式化证明)指出:三者不可同时满足。
实际上,分区容忍(P)在互联网分布式系统中是必选项——网络延迟、丢包、节点隔离随时发生,放弃 P 意味着放弃分布式本身。因此真正的选择是:
网络分区发生时:
→ 保 C(拒绝写入/返回错误,等待一致性恢复) —— CP 系统
→ 保 A(继续响应,接受数据可能过时) —— AP 系统3. CA、CP、AP 的现实映射
| 组合 | 典型场景 | 代表系统 |
|---|---|---|
| CA(无分区假设) | 单机数据库 / 同机房强同步 | 单节点 MySQL,PostgreSQL 单主 |
| CP | 要求强一致,分区时宁可拒绝服务 | ZooKeeper、etcd、HBase |
| AP | 要求高可用,分区时允许数据短暂不一致 | Cassandra、Eureka、CouchDB |
4. 选型决策树
业务能接受"读到旧数据"吗?
→ 否(金融账务、配置中心、分布式锁)→ 选 CP
→ 是(购物车、点赞数、服务注册) → 选 AP5. CAP 的局限
- CAP 只描述分区时的极端场景,正常运行时 C 与 A 并不冲突
- "一致性"在 CAP 中特指线性一致性(最强模型),工程中常用更弱的最终一致性满足需求
- PACELC 模型在此基础上补充了延迟维度(见独立题目)
考察意图
考察候选人是否理解 CAP 不是"随意选两个"的排列题,而是在分区容忍必选的前提下,根据业务对数据新鲜度的容忍程度在 C 与 A 之间做出选择;同时考察能否识别 CAP 的适用边界(线性一致性 vs 最终一致性的概念区分)。
追问链
"CA 系统"是否真实存在?
严格意义上不存在:只要是分布式系统就必须处理网络分区,放弃 P 等价于放弃分布式。实践中"CA"通常指单节点或同机房同步复制的场景,一旦网络发生分区就必须牺牲 A 或 C。
CAP 中的 C 与 ACID 中的 C 有何区别?
ACID 的 C(一致性)指事务执行前后业务约束不被破坏,是应用层语义;CAP 的 C(一致性)特指线性一致性,即所有节点对同一数据的读写像在单机上执行一样有序,是系统层保证。两者完全不同,面试中混淆是典型失分点。
AP 系统如何弥补"不一致"带来的数据问题?
通过 BASE 策略:①版本向量 / 时间戳检测冲突;②Last-Write-Wins 或业务级合并(如购物车合并)解决冲突;③异步补偿(对账、补单)修复最终数据;④向用户展示"数据可能稍有延迟"的提示。
易错点
把 P 当可选项:认为可以"选 CA 放弃分区容忍"。现实中分布式系统必须处理网络分区,"放弃 P"实际意味着退化为单机,CAP 的真正选择只在 C 与 A 之间。
混淆 CAP-C 与 ACID-C:CAP 的一致性是线性一致性(系统层),ACID 的一致性是业务约束不变性(应用层),两者含义、层次均不同。
认为 CP 系统完全不可用:CP 系统在分区时会拒绝部分写请求以保证一致性,但正常运行时完全可用;不是"分区=宕机",而是"分区=降级为只读或报错"。
代码示例
// 模拟 CP 行为:写操作在无法确认多数节点时拒绝服务
class CpKvStore
{
private array $nodes;
private int $quorum;
public function __construct(array $nodes)
{
$this->nodes = $nodes;
$this->quorum = (int) floor(count($nodes) / 2) + 1; // 多数派
}
/** @throws \RuntimeException 分区时拒绝写入以保证一致性(CP 策略) */
public function set(string $key, string $value): void
{
$acks = 0;
foreach ($this->nodes as $node) {
if ($node->write($key, $value)) {
$acks++;
}
}
if ($acks < $this->quorum) {
// 未达多数派确认 → 拒绝(牺牲 A,保住 C)
throw new \RuntimeException("写入未达 quorum({$acks}/{$this->quorum}),操作拒绝");
}
}
}