本节摘要:CAP 定理是分布式系统的第一块基石:在网络分区必然发生的前提下,一致性与可用性只能二选一。它常被误读成"给系统贴一个 CP 或 AP 的标签",其实它描述的是分区发生的那个瞬间。无分区时,PACELC 定理补上了延迟与一致性之间的日常权衡。再把一致性从"一个开关"展开成"一条光谱"——线性一致、顺序一致、因果一致、最终一致——你就有了判断任何分布式系统行为的标尺。
阅读完本节,你应当能够:
先回到一个朴素的起点:我们为什么非要把数据拆到多台机器上?
最直接的原因是装不下。单机的磁盘有上限,内存有上限,就算换上最贵的机器,物理极限也摆在那里。第二层原因是扛不住:一台机器每秒能处理的请求数是有限的,CPU 核数、网卡带宽、磁盘 IO 都是天花板。第三层原因更隐蔽,是怕坏掉——再可靠的机器,也迟早会宕机、会断电、会硬盘损坏。把数据只放在一台机器上,等于把整个业务的命押在一张牌上。
于是我们把数据切成片,分到不同的机器上;再给每一份数据抄几份副本,放到不同的机器上。这一拆一抄,看似简单,却把我们从单机的确定性世界里,一脚踢进了一个充满不确定性的世界。多台机器之间要靠网络通信,而网络会延迟、会丢包、会彻底断开;多台机器各自有自己的时钟,而时钟会漂移;任何一台机器都可能在任意时刻崩溃,而你无法立即知道它是真崩了还是只是"慢"。
一句话:单机数据库是街角那家只有一个窗口的邮局,你要寄信,走到窗口,办完,走人,因果清晰。分布式系统是一张遍布全城的邮政网络,有几十个网点,信可能从任何网点进出,网点之间还可能因为暴雨断路。这时候,"这封信到底寄出去没有"这个问题,突然变得没那么好回答了。
这一节要讲的,就是在这样的不确定性里,我们还能不能、以及如何谈论"正确"。
2000 年前后,埃里克·布鲁尔提出一个猜想,两年后被吉尔伯特和林奇严格证明,这就是 CAP 定理。它的意思,用大白话说就是:一个跨越多台机器、靠网络通信的分布式系统,不可能同时做到下面三件事——
关键在第三点。很多人以为分区容忍性是一个可以勾选或取消的选项,其实不是。只要你的系统跑在多台机器上,网络分区就迟早会发生:光纤被挖断、交换机重启、机房断电、云厂商可用区之间的链路抖动。分区不是"会不会发生"的问题,而是"什么时候发生"的问题。所以 P 是前提,你没法放弃它,真正能选的,只有分区发生时你站在 C 那边,还是 A 那边。
这就引出了 CAP 最常被误读的地方。我们经常听到"某某数据库是 CP 的,某某是 AP 的",这种说法把系统简化成了贴标签。实际上,CAP 描述的是系统在分区发生的那一瞬间的行为,不是它平时的常态。一个平时读写飞快、强一致的数据库,在分区发生时可能选择拒绝写入(站在 C 这边);另一个平时用最终一致的系统,在分区时可能选择继续服务、接受读到旧数据(站在 A 这边)。分区一旦恢复,它们又回到各自的正常状态。
我更愿意把 CAP 想成"分区时刻的选择题",而不是"系统的属性卡"。这样想,你才不会拿着一个 CP 标签去给系统打包票,也不会因为一个系统在分区时选择了可用性,就断定它永远不一致。
CAP 只覆盖了"分区发生"这一种情况。可现实里,分区是少数时候,大多数时候网络是好的。没分区的时候,系统就高枕无忧了吗?没有。就算网络通畅,一个跨洲的读请求,往返延迟也要上百毫秒。这时候你要不要为了强一致,等最慢的那个副本回话?这就是 PACELC 定理要补的空白:没有分区时(Else),你还要在延迟(Latency)和一致性(Consistency)之间做选择。
于是分布式系统要做的,其实是在两个场景里分别做一次选择:分区时选 C 还是 A,没分区时选低延迟还是强一致。这两个选择不必一致。一个系统完全可以在平时追求低延迟、读本地副本,而分区一发生就立刻转成强一致、拒绝写入。理解了这一点,你就理解了很多数据库那些看似矛盾的行为:平时很快,故障时很"倔"。
CAP 里的"一致性"指的是最强的那个等级。但现实中,不是每个场景都需要那么强。把一致性当成一个布尔值,非黑即白,是最容易犯的错。它其实是一条光谱,从强到弱排列着多个被精确定义的等级。
最顶端是线性一致。它要求系统表现得像一台单机:任何一次读,都必须返回它开始之后、下一个写开始之前某个时刻的最新值;所有操作可以被排成一个符合真实时间顺序的序列。这是程序员的直觉延伸,也是最贵的一致性,因为它往往要求跨节点协调,牺牲延迟。
往下是顺序一致。它不要求"实时可见",但要求所有进程看到的操作顺序,和某个全局的串行顺序一致,且每个进程内部的操作顺序保持自己的程序顺序。它禁止了"乱序的幻觉",但允许一个操作在真实时间里已经完成,别的进程却过一会儿才看到。
再往下是因果一致。它只保证有因果关系的操作被按先后顺序看到:如果 B 读到了 A 写的数据,那么所有节点必须先看到 A,再看到 B。没有因果关系的操作可以并发、可以乱序。这更贴近人类的直觉——前因后果不能颠倒,但两件无关的事谁先谁后无所谓。
最后是最终一致。它只承诺:如果系统不再有新的写入,那么经过足够长的时间,所有副本最终会收敛到同一个值。中间的过程可以有旧数据、可以有短暂的分歧。这是最宽松的承诺,也是很多互联网系统在默认情况下给的东西。
在这条光谱上,还有两个针对单客户端体验的实用保证值得单独拎出来:单调读,保证一个客户端不会读到"倒退"的旧版本,读到新值之后不会再读回旧值;读己之所写,保证一个客户端写完一条数据后,它自己随后的读一定能看到这条写入。这两个是 Web 应用最基本的心理契约——你刷新页面,至少不该看到自己刚才提交的东西凭空消失。
| 一致性模型 | 核心承诺 | 典型代价 | 适用场景 |
|---|---|---|---|
| 线性一致 | 像单机一样,实时看到最新值 | 高延迟、需协调 | 分布式锁、选主、金融扣款 |
| 顺序一致 | 全局顺序一致但可延迟可见 | 中等协调开销 | 共享内存、部分缓存 |
| 因果一致 | 有因果的操作有序,无关的可乱序 | 需携带因果元数据 | 社交时间线、协作编辑 |
| 最终一致 | 停止写入后终会收敛 | 可能读到旧值、需处理冲突 | 商品详情、评论、DNS |
| 单调读 | 单客户端不倒退 | 需按客户端绑定副本 | 用户会话内的连续读 |
| 读己之所写 | 自己写的自己必能读到 | 需追踪自身写入 | 表单提交后刷新 |
⚠️ 常见坑:把"最终一致"当成"不需要处理冲突"。最终一致只保证"最终会一致",不保证"怎么一致"。两个节点同时改了同一个字段,谁覆盖谁?这个问题系统不会自动替你回答,你必须用版本号、时间戳或业务规则去消解冲突。忽略它,上线后就会冒出"订单状态被旧值覆盖"这类事故。
💡 关键直觉:选择一致性等级,本质是在为"读到的数据有多新"这件事标价。你不该默认上最强的一致,也不该偷懒全用最终一致。问自己一句:这个读操作,读到旧值会造成什么后果?后果越严重,越该往上走;后果可以容忍或可以补偿,就往下走。
光谱是方向,工程上还需要一把尺子。这把尺子就是法定人数,也就是我们常说的读写副本数 W 和 R。
假设一份数据有 N 个副本。每次写,要等 W 个副本确认才算成功;每次读,要从 R 个副本里读。只要保证 W 加 R 大于 N,那么任何一次读,至少会碰到一个"参与了最近那次成功写"的副本,于是你读到的一定包含最新值。这个不等式看起来简单,却是很多系统的一致性根基。
反过来调这两个参数,你就在一致性光谱上移动。写副本数取 1、读副本数取 N,写最快,但读最贵,而且读到的未必最新;写副本数取 N、读副本数取 1,读最快最便宜,但写要等所有副本,慢且脆弱。实际系统会在这两个极端之间找平衡:比如三副本里写二读二,写要等多数派,读也要等多数派,既保证读新,又容忍一个副本挂掉。
这个机制也解释了为什么"多数派"这么重要。选主、提交日志、判断提交与否,都依赖"过半节点同意"这个条件。因为任意两个过半的集合必有交集,这保证了任意两轮多数派决策之间能"传递记忆"——上一轮的决定,一定被下一轮的某个节点看到。
💡 关键直觉:法定人数不是越多越好。W 和 R 调大,一致性变强,但延迟变高、可用性变差;调小,延迟低,但可能读到旧值。这是一根可以实时拨动的旋钮,很多系统允许每个请求单独指定,而不是全局写死。
理论落到工程,就是看你在具体场景里怎么拨这根旋钮。
场景一:电商库存扣减。 库存不能超卖,这几乎是硬约束。但如果我们每笔扣减都跨节点做线性一致的协调,大促期间就会慢到不可用。常见的做法是分层:缓存层用原子递减做快速预扣,接受一定程度的近似,靠异步消息把最终扣减落到数据库,再用定时对账和补偿把不一致抹平。这里的"一致性"被拆成了两段:前台要的是低延迟的近似一致,后台要的是最终收口的强一致。
场景二:金融转账。 转账不能"先扣后没加上",也不能"两边都扣了"。这是强一致的地盘,宁可在分区时拒绝服务,也不能出现账目不一致。所以金融核心系统普遍选择分区时站在一致性这边,牺牲可用性来保账本正确。
这两个场景告诉我们,CAP 和一致性光谱不是用来背的教条,而是用来描述"你到底能接受什么"的语言。有了这层语言,你才能在白板上把一次读操作到底容忍多少陈旧、一次写操作到底要等几个副本,谈得清清楚楚。
有了这套语言,下一节我们去看它怎么落地——数据如何被切成片、抄成多份,以及分片键和副本数这些具体旋钮怎么选。