深蓝黑背景封面:左侧大标题「分区一旦发生,C 和 A 只能留一个」,副标题「一致性、可用性与分区容错的取舍框架」;右侧原创示意图——一个三角形三个顶点分别标着 C、A、P,P 到底下两个顶点之间的边被一道闪电状裂缝劈开,示意网络分区时只能在 C 与 A 之间二选一;左上标注「面试经典 · 系统设计」,右上标注「Day 10 · 10-28」 图 1:本文封面,分区一旦发生,C 和 A 只能留一个。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。题面:什么是 CAP?为什么说分布式系统只能三选二?

先把结论立住:**CAP 不是常态下的属性标签,而是故障时刻的取舍预案——设计时要回答的问题是「断网那几分钟,你的系统宁可牺牲什么」。**2000 年 Brewer 提出猜想,2002 年 Gilbert 与 Lynch 给出形式化证明:网络共享数据系统无法同时满足一致性、可用性与分区容错。原论文对三词的定义是解题的地基:一致性指所有节点同一时刻看到同一份数据,可用性指每个请求都能在有限时间内得到非错误响应,分区容错指节点间消息丢失后系统仍要继续运转。

**先破掉「三选二」的误读。**网络分区在分布式系统里迟早发生——光纤被挖断、交换机故障、机房断网,都不是「会不会」而是「什么时候」。所以 P 不是可选项而是必选项,真正的选择只在分区发生的那一刻才出现:两侧节点连不上,一个写请求来了,你要么拒绝它(保一致,牺牲可用),要么各自应答(保可用,牺牲一致)。常态下 C 和 A 可以兼得,「三选二」描述的从来是分区期间。

**选 C 的一侧归为 CP。**宁可部分节点不可用,也不返回不一致数据。典型手段是多数派:写必须过半确认,拿不到过半的分区拒绝服务。元数据、账务、配置中心这类「错一次就出事」的场景走这条路。

**选 A 的一侧归为 AP。**各分区独立应答,先保证服务在线,愈合后再对账合并。缓存、浏览型数据、内容分发走这条路——短期读到旧值可以忍,服务不在线不能忍。代价是分区期间的并发写会分叉,愈合后必须靠补偿机制收场。

定义加决策路径图:上方三张卡片先正名——一致性指所有节点同一时刻看到同一份数据,可用性指每个请求都能在有限时间内得到非错误响应,分区容错指节点间网络断开后系统仍要继续运转;并点破「三选二」的常见误读:网络分区在分布式系统里迟早发生,P 不是可选项而是必选项,真正的选择只在分区发生的那一刻才出现。中间决策路径:分区发生 → 选 C 的一侧拒绝写入或读旧值之外的一切,宁可部分节点不可用也不返回不一致数据,归为 CP;选 A 的一侧各分区独立应答,先保证服务在线,恢复后再对账合并,归为 AP。下方对照:CP 适合强一致场景如元数据与账务,代价是分区期间部分不可用;AP 适合在线服务如缓存与浏览型数据,代价是短期读到旧值需要补偿机制。底部提示:CAP 不是常态下的属性标签,而是故障时刻的取舍预案——设计时要回答的是「断网那几分钟,你的系统宁可牺牲什么」 图 2:三个词的定义与分区时刻的取舍路径。

「分区时 C 和 A 互相排斥」可以用一个最小仿真看清楚。下面用 Node.js 22.15.0 模拟一个双节点存储:正常期写入同步到两节点;分区后分别按 CP 与 AP 两种策略处理写请求(node cap.js):

'use strict';

// 最小分布式存储:两个节点互相同步写入。模拟网络分区时 CP 与 AP 两种取舍。
function makeCluster(mode) {
  const nodes = [
    { name: 'N1', data: {}, log: [] },
    { name: 'N2', data: {}, log: [] },
  ];
  let partitioned = false;
  return {
    partition() { partitioned = true; },
    heal() {
      partitioned = false;
      // 分区愈合后的对账:last-write-wins,后写的覆盖先写的
      for (const [k, v] of Object.entries(nodes[0].data)) nodes[1].data[k] = v;
      for (const [k, v] of Object.entries(nodes[1].data)) nodes[0].data[k] = v;
      // 合并后两节点取并集,但同 key 的并发写只有一个能活
      const merged = { ...nodes[0].data, ...nodes[1].data };
      nodes[0].data = { ...merged };
      nodes[1].data = { ...merged };
    },
    write(target, key, value) {
      const node = nodes[target];
      const other = nodes[1 - target];
      if (!partitioned) {
        node.data[key] = value;
        other.data[key] = value;
        return `OK(已同步到两个节点)`;
      }
      if (mode === 'CP') {
        // 选 C:无法确认对端状态就拒绝写,宁可不可用也不产生分歧
        return `拒绝(分区期间无法保证一致,写入失败)`;
      }
      // 选 A:本分区独立应答,先记下,分歧留给愈合后对账
      node.data[key] = value;
      node.log.push(`${key}=${value}`);
      return `OK(仅写入 ${node.name},与对端产生分歧)`;
    },
    read(target, key) {
      return nodes[target].data[key] ?? null;
    },
    consistent() {
      const [a, b] = nodes.map((n) => JSON.stringify(Object.keys(n.data).sort().map((k) => [k, n.data[k]])));
      return a === b;
    },
  };
}

// CP 演示
const cp = makeCluster('CP');
console.log('—— CP 取舍(分区期间宁可不可用)——');
console.log('正常期写 config=a:' + cp.write(0, 'config', 'a'));
cp.partition();
console.log('分区后 N1 写 config=b:' + cp.write(0, 'config', 'b'));
console.log('分区后 N2 写 config=c:' + cp.write(1, 'config', 'c'));
console.log('两节点数据一致:' + cp.consistent() + '(都停在 config=a)');

// AP 演示
const ap = makeCluster('AP');
console.log('—— AP 取舍(分区期间保持在线)——');
console.log('正常期写 config=a:' + ap.write(0, 'config', 'a'));
ap.partition();
console.log('分区后 N1 写 config=b:' + ap.write(0, 'config', 'b'));
console.log('分区后 N2 写 config=c:' + ap.write(1, 'config', 'c'));
console.log('分区期间从 N1 读到:' + ap.read(0, 'config') + ',从 N2 读到:' + ap.read(1, 'config'));
console.log('两节点数据一致:' + ap.consistent() + '(分歧已发生)');
ap.heal();
console.log('愈合对账后从 N1 读到:' + ap.read(0, 'config') + ',从 N2 读到:' + ap.read(1, 'config'));
console.log('愈合后两节点一致:' + ap.consistent() + '(其中一次写入被对账覆盖,需要补偿机制兜底)');

实测输出:

—— CP 取舍(分区期间宁可不可用)——
正常期写 config=a:OK(已同步到两个节点)
分区后 N1 写 config=b:拒绝(分区期间无法保证一致,写入失败)
分区后 N2 写 config=c:拒绝(分区期间无法保证一致,写入失败)
两节点数据一致:true(都停在 config=a)
—— AP 取舍(分区期间保持在线)——
正常期写 config=a:OK(已同步到两个节点)
分区后 N1 写 config=b:OK(仅写入 N1,与对端产生分歧)
分区后 N2 写 config=c:OK(仅写入 N2,与对端产生分歧)
分区期间从 N1 读到:b,从 N2 读到:c
两节点数据一致:false(分歧已发生)
愈合对账后从 N1 读到:b,从 N2 读到:b
愈合后两节点一致:true(其中一次写入被对账覆盖,需要补偿机制兜底)

CP 分支用「拒绝写入」守住了一致性;AP 分支分区期间两边各写各的,config 在 N1 是 b、在 N2 是 c,分歧真实发生,愈合后 last-write-wins 只保住一个值,另一次写入永远丢失——这就是 AP 必须配补偿机制的原因。

**常见误区。**一是把 CAP 当常态标签背「某某系统是 CA」——不存在分区时谈取舍没有意义;二是把一致性混同为数据库 ACID 的 C,CAP 的 C 指多节点视角的线性一致,不是事务约束;三是以为 AP 就是「不要一致性」,它是把一致性推迟到愈合后,并且要为丢写付代价;四是以为多数派能同时保住 C 和 A——多数派保住的只是多数侧的可用,少数侧照样不可用。

AI 工程里向量库、特征存储、模型注册中心的选型都要回答「分区时选一致还是可用」,CAP 是分布式组件选型的第一性框架。答题节奏:先正名三词,再破「三选二」误读,然后给出分区时刻的 CP/AP 决策路径,最后落到「断网时宁可牺牲什么」的预案。你负责的系统在分区预案上选了哪一边?欢迎分享。