17.6 CAP 之后的补充理论

一句话

CAP 只说“分区时 C/A 二选一”,之后二十年补的是三件事:可用性是连续的(harvest / yield)、延迟也要记账(PACELC)、以及哪些操作根本不需要协调(CALM / I-confluence / HAT)。

一张表看完这二十年

理论 年份 补了 CAP 的哪个盲点 工程含义
Harvest / Yield 1998 可用性不是布尔,是“回答多少请求、回答里含多少数据” 降级设计:少返回一些,也好过不返回
BASE 2008 把“弱一致”变成一套可执行的工程纲领 中间态、补偿、幂等、对账(见 17.5 BASE 与柔性事务)
CALM 2010 哪些计算天生不需要协调 单调程序可免协调;非单调必须协调
causal+ 2011 分区期间能实现的最强一致性是什么 因果一致 + 收敛,是“始终可用”的上限
CRDT 2011 用什么数据结构可以无冲突合并 计数器、集合、寄存器……见 25.6 CRDT (计数 / 集合 / 寄存器)
PACELC 2012 无分区时也要在延迟与一致性之间选 见 17.4 PACELC
HAT 2013 哪些隔离级别可以做到“高可用” 读已提交可以;快照隔离/可串行化不行
I-confluence 2014 哪些不变式可以“不协调也不破坏” 余额 ≥ 0 不行;计数 ≥ 0 可以

Harvest / Yield:可用性是连续的

CAP 的 A 是“每个请求都必须返回”,非 0 即 1。现实中更常用的是两个连续量:

yield   = 被回答的请求 / 总请求        (服务了多少人)
harvest = 回答中包含的数据 / 应有数据   (答案有多完整)
场景 yield harvest 用户感受
正常 100% 100% 无感
一个分片挂了,返回其余分片的结果 100% 90% 搜索少几条,但能用
只读降级 100%(读)/ 0(写) 100% 能看不能改
熔断 20% 100% 大部分请求被快速拒绝,但没拖垮系统
全挂 0 0 不可用

🔑 “基本可用”不是“凑合能用”,而是主动选择在哪个维度降级:宁可 harvest 少一点,也要保住 yield;还是宁可拒绝一部分请求(yield 降),也要保证答案完整。这是产品决策,不是数据库参数。

causal+:始终可用的系统里,最强能做到因果一致

Mahajan、Alvisi、Dahlin 2011 年的结果可以记成一句话:在“始终可用 + 分区容忍”的约束下,因果一致(加上收敛,即 causal+)是能实现的最强一致性模型。

为什么不是线性一致?因为线性一致要求“读看到最新写”,这需要协调,而协调在分区时必然牺牲可用性(17.1 CAP 的由来与准确表述 的证明)。因果一致不要求“最新”,只要求“不违反因果”:如果你看到了我回复你的消息,那么你一定也看得到我回复所依据的那条消息(“因果”就是 happened-before 偏序,定义见 16.4 因果一致性 与 23.6 happened-before 关系)。

工程含义:

  • 想“分区两侧都能写”,上限就是因果一致(再往上必须付协调成本);
  • 因果一致的实现通常靠版本向量 / 逻辑时钟(见 23.4 向量时钟),读时等待“因果依赖已到齐”;
  • 它不保证“读到最新”,所以读己之写仍要额外的会话保证(16.6 会话保证)。

HAT:弱隔离可以高可用,强隔离不行

Bailis 等人 2013 年的《Highly Available Transactions》系统性地回答了“哪些事务隔离级别可以在分区时保持高可用”:

隔离级别 / 保证 能否高可用(HAT) 原因
读已提交(Read Committed) ✅ 不需要跨副本定序
单调原子视图(MAV) ✅ 只要求“看到原子提交的中间状态”,不要求全局序
快照隔离(SI) ❌ 需要全局快照或冲突检测,跨分区要协调
可串行化(Serializable) ❌ 需要全局序,等价于要求协调
线性一致读 ❌ CAP 的直接推论

⚠️ 这张表最容易被误读成“弱隔离就一定高可用”。准确说法是:HAT 给出的是“在始终可用的前提下,你最多能拿到什么”;具体实现仍要看你是否真的避免了跨分区协调。很多号称“用了快照隔离还很可用”的系统,其实是在分区时降级(拒写、切主),也就是牺牲了 CAP 的 A。

CALM:单调的计算不需要协调

CALM 定理(Hellerstein 2010 提出,Ameloot 等人 2011 年形式化)说的是:一个计算如果能用单调逻辑表达,就存在一个“不需要协调”的分布式实现。

直觉版:如果“多知道一点信息”永远不会让已经得出的结论变错,那么这个结论就可以各自算、最后合并;反之,就必须先协调。

计算 单调? 能不能免协调
求和 ✅(加法可交换、可结合) 能:各分区求和再相加
求最大值 ✅ 能:各分区取 max 再取 max
集合并 ✅(幂等) 能:合并取并集
去重计数 ❌ 不能:两边各数一遍再相加会重复计数
“余额 ≥ 0”检查 ❌ 不能:结论依赖“当前值”,并发下会反转
“用户名唯一”检查 ❌ 不能:两个分区各自都认为没冲突

实测一下最能说明问题:

package main

import "fmt"

func main() {
	// 两个分区各自看到一部分写入
	aUsers := []string{"u1", "u2", "u3"}
	bUsers := []string{"u3", "u4"}
	aAmount, bAmount := 300.0, 200.0

	fmt.Printf("分区 A:%v,金额 %.0f\n", aUsers, aAmount)
	fmt.Printf("分区 B:%v,金额 %.0f\n", bUsers, bAmount)

	// ① 求和:单调,直接相加就是对的
	fmt.Printf("\n① 金额求和:%.0f + %.0f = %.0f → 合并正确,无需协调\n",
		aAmount, bAmount, aAmount+bAmount)

	// ② 去重计数:非单调,直接相加会重复计数
	set := map[string]bool{}
	for _, u := range append(append([]string{}, aUsers...), bUsers...) {
		set[u] = true
	}
	fmt.Printf("② 去重计数:|A|+|B| = %d,真值 %d → 直接相加错了 %d\n",
		len(aUsers)+len(bUsers), len(set), len(aUsers)+len(bUsers)-len(set))
	fmt.Printf("   改成集合求并(幂等、可交换)→ %d,正确;这就是 CRDT 的并集\n", len(set))

	// ③ 不变式:两个分区各自检查都通过,合并后违反
	balance, withdraw := 100.0, 60.0
	fmt.Printf("\n③ 余额 %.0f,两个分区各收到一笔取款 %.0f:\n", balance, withdraw)
	fmt.Printf("   分区 A 检查 %.0f - %.0f >= 0 → 通过\n", balance, withdraw)
	fmt.Printf("   分区 B 检查 %.0f - %.0f >= 0 → 通过\n", balance, withdraw)
	fmt.Printf("   合并后 %.0f - %.0f - %.0f = %.0f → 不变式「余额 >= 0」被破坏\n",
		balance, withdraw, withdraw, balance-withdraw-withdraw)
}

输出:

分区 A:[u1 u2 u3],金额 300
分区 B:[u3 u4],金额 200

① 金额求和:300 + 200 = 500 → 合并正确,无需协调
② 去重计数:|A|+|B| = 5,真值 4 → 直接相加错了 1
   改成集合求并(幂等、可交换)→ 4,正确;这就是 CRDT 的并集

③ 余额 100,两个分区各收到一笔取款 60:
   分区 A 检查 100 - 60 >= 0 → 通过
   分区 B 检查 100 - 60 >= 0 → 通过
   合并后 100 - 60 - 60 = -20 → 不变式「余额 >= 0」被破坏

I-confluence:不变式能不能“不协调也不破”

CALM 说的是“计算”,I-confluence(Bailis 2014)把它推到“不变式”:如果任意顺序、任意切分地应用所有操作,不变式都不会被破坏,那么这份数据可以不做协调。

不变式 I-confluent? 例子 / 反例
计数 ≥ 0(只增) ✅ 点赞数、访问计数:合并取和/max,永不为负
追加日志的顺序 ✅ 只追加、不更新,合并即拼接
库存 ≥ 0 ❌ 两个分区各卖 1 件,合并后超卖(见 17.3 分区期间的取舍)
余额 ≥ 0 ❌ 上面的实测:-20
用户名唯一 ❌ 两侧都能注册 alice,合并时冲突
每个用户最多 3 台设备 ❌ 两侧各绑 3 台,合并后 6 台

🔑 判断口诀:“先算后合”会不会改变结论? 会,就必须协调;不会,就可以各自算、最后合。是否单调、是否 I-confluent,是比“选 CP 还是 AP”更细、更有用的判断。

这些性质最终落到“这条数据允许读到多旧的值”,谱系见 16.7 一致性模型谱系图。

把它们放回工具箱

问题 用什么回答 结论
分区时少数派怎么办? CAP / 17.3 分区期间的取舍 拒写、照写、还是只读/排队
平时要不要付跨副本延迟? PACELC / 17.4 PACELC EC 付延迟,EL 付陈旧与合并
这条数据要不要协调? CALM / I-confluence 单调 & I-confluent → 免协调
一致性最多能做到多强? CAP + causal+ 始终可用 ⇒ 上限是因果一致
弱隔离能不能高可用? HAT 读已提交可以,SI/可串行化不行
故障时“部分可用”怎么衡量? Harvest / Yield 分别盯 yield 与 harvest
共识为什么这么难? FLP(见 22.2 FLP 不可能性) 异步 + 崩溃故障下,确定性共识无法保证终止——这是另一个问题,不是 CAP

⚠️ 最后一行值得强调:CAP 与 FLP 不是一回事。CAP 讲“分区时一致性与可用性二选一”,FLP 讲“异步网络里,确定性共识算法不能保证终止”。它们常被混着引用,但推导方向完全不同:Raft 是 CP 的(分区时少数派不写),同时它也绕开了 FLP 的“必须终止”要求(靠随机超时与选举,允许“暂时选不出主”)。

判断流程:从数据到策略

① 这个操作是单调的吗?(只增、可交换、幂等)
   ├─ 是 → 免协调:AP/EL,用 CRDT/集合并/计数
   └─ 否 → ②
② 不变式是 I-confluent 吗?(先算后合会不会破坏它)
   ├─ 是 → 可以分区,但要在读时或合并时校验
   └─ 否 → ③
③ 能否缩小协调范围?
   ├─ 能 → 单 key / 单分片 / 单地域内协调(分段发号、单元化)
   └─ 不能 → ④
④ 必须协调:选 EC/CP,接受延迟或分区时不可用;
   同时设计降级、对账、补偿

第 ④ 步的降级、对账、补偿具体怎么做,见 17.5 BASE 与柔性事务。

反模式

反模式 后果
拿定理当架构:“我们符合 CALM,所以不用协调” CALM 只对单调计算成立,业务里大量操作不单调
用 CRDT 表示资金 资金需要“减法”与“不小于零”,不满足 I-confluence
认为“弱隔离 = 高可用” SI 与可串行化都不在 HAT 里
把 CAP 与 FLP 混着引用 结论推错方向:一个讲 C/A,一个讲终止性
只盯 yield 不盯 harvest 每个请求都回 200,但内容缺一半,用户照样不能用
以为因果一致“没有代价” 要版本向量、要等依赖到齐,读延迟与元数据成本都在
认为“最终一致”可以没有时间界 没有界就没有 SLA,也没有对账的触发条件

自检

Q1 为什么“去重计数”不满足 CALM 的免协调条件?给出两种能改成“可合并”的写法。

▸ 答案:因为去重计数不是单调的:合并两个分区时把各自的计数相加会把重复元素数两遍(实测 3+2=5,真值 4),而“知道更多信息”会让已有结论变错。改法:① 合并集合(并集是幂等、可交换、单调的),基数在读取时计算——代价是存储元素本身;② 用 OR-Set / 唯一 ID 集合 之类的 CRDT,元素带唯一标识,合并按 ID 去重,基数同样在读取时取。两种写法的共同点:把“计数”这个非单调运算,换成“集合合并”这个单调运算。

Q2 同样是“不能为负”,“余额 ≥ 0”不 I-confluent,而“点赞数 ≥ 0”可以。差别在哪?

▸ 答案:差别在是否存在减法与依赖当前值的判定。点赞数只增(或只做 max/求和),任意顺序合并不可能为负;余额要扣减,判定“扣完是否 ≥ 0”依赖当前值,两个分区各自看到 100、各扣 60,合并后是 -20(实测),不变式被破坏。一般规律:只增/只取 max 的量天然 I-confluent;涉及减法、唯一性、容量上限的量,通常需要协调,或者改造成“预留 + 最终确认”。

Q3 HAT 说快照隔离不能高可用,那为什么一些系统“用了快照隔离还很可用”?

▸ 答案:两种可能。① 它们在分区时降级:少数派拒写或切主,牺牲了 CAP 的 A,只是工程上做了快速失败与重试,用户感知不明显;② 它们的快照隔离只在单个分区内生效(单分片、单地域),跨分片的快照并没有真正提供。也就是说,“可用”是在业务层用降级换来的,不是隔离级别本身带来的。判断方法:看分区时少数派的写是不是被拒绝了。

Q4 用一句话区分 CAP、PACELC、FLP 三个“不可能性/权衡”结论,并说明它们各自不能推出什么。

▸ 答案:CAP:分区期间,线性一致与“每个请求都返回”不可兼得(不能推出“平时也要牺牲一致性”);PACELC:无分区时,延迟与一致性仍需取舍(不能推出“分区时可以两者都要”);FLP:异步网络中只要有一个进程可能崩溃,确定性共识就无法保证终止(不能推出“共识不可用”——工程上用超时、随机化、部分同步假设绕开)。三者分别约束“分区时”“平时”“终止性”,互相不能替代。

关联