以实用视角理解 Eric Brewer 的 CAP 定理:一致性、可用性与网络分区如何塑造分布式系统的设计决策。

当你在多台机器上保存相同的数据时,你得到的是更快的响应和更强的容错能力——但同时也带来了一个新问题:不一致。两台服务器可能收到不同的更新,消息可能迟到或根本丢失,用户可能根据命中的是哪个副本看到不同的结果。CAP 之所以流行,是因为它给工程师提供了一个清晰的方式来描述这种混乱的现实,而不是含糊其辞。
Eric Brewer(一位计算机科学家、Inktomi 的联合创始人)在 2000 年提出了这一核心想法,作为在故障情况下对复制系统的实用陈述。它迅速传播,因为它吻合了团队在生产环境中已经在经历的事情:分布式系统的失败并不只是“宕机”;它们会通过分裂而失败。
CAP 在事情出错时最有用——尤其是当网络不按预期工作时。在健康的日子里,许多系统看起来既一致又可用。真正的考验是当机器不能可靠通信时:你必须决定在系统分裂期间如何处理读写请求。
这个框架正是 CAP 成为常用思维模型的原因:它不讨论最佳实践;它逼你回答一个具体问题——在分区时我们会牺牲什么?
到本文结尾,你应该能够:
CAP 持久不衰,因为它把模糊的“分布式很难”讨论,变成了一个你可以做出并捍卫的决定。
分布式系统,用通俗的话说,就是 许多计算机试图表现得像一台。你可能在不同的机架、区域或云可用区中有几台服务器,但对用户来说就是“这个应用”或“这个数据库”。
为了让共享系统在真实世界规模下工作,我们通常会复制:在不同机器上保存同一数据的多个副本。
复制流行有三个实用原因:
到这里看起来复制像是直接的收益。问题在于,复制带来了一个新任务:让所有副本保持一致。
如果每个副本都能即时与其他副本通信,它们就能协调更新并保持一致。但真实网络并不完美。消息可能延迟、丢失或因故障而绕开路由。
当通信正常时,副本通常可以交换更新并趋于一致。但当通信中断(即便是暂时的)时,你可能会得到两个看似有效的“真相”版本。
例如,用户更改了送货地址。副本 A 收到了更新,副本 B 没有。现在系统必须回答一个看似简单的问题:当前地址是什么?
这就是以下两种状态的不同:
CAP 思维正是从这里开始:既然存在复制,在通信失败时产生分歧不是边缘情况——而是核心设计问题。
CAP 是一个用来描述当系统分布在多台机器(通常在多个地点)时用户实际感受的思维模型。它不是在评价“好”或“坏”的系统——而是你必须管理的张力。
一致性关乎达成一致。如果你更新了某项内容,下一次读取(无论从哪个副本)是否会反映该更新?
从用户角度看,这区别在于“我刚改了,大家都看到新值”与“有人在一段时间内仍看到旧值”。
可用性意味着系统对请求——读和写——返回成功结果。不是“尽可能快”,而是“不拒绝为你服务”。
在出问题时(服务器宕机、网络抖动),可用的系统会继续接受请求,即便返回的数据可能稍有过时。
分区就是网络分裂:机器在运行,但它们之间的消息无法通过(或到达太晚以至于无用)。在分布式系统中,你不能把这当作不可能事件——你必须定义在这种情况下的行为。
想象两家零售店都卖同一件商品并共享“1 件库存”。顾客在店 A 买走了最后一件,店 A 写入 inventory = 0。与此同时,网络分区使店 B 听不到这个消息。
如果店 B 选择保持可用性,它可能会售出一件它实际上没有的商品(在分区时接受销售)。如果店 B 强制一致性,它可能会拒绝售出直到确认最新库存(在分区期间拒绝服务)。
“分区”不仅仅是“互联网断了”。它是指系统的部分不能可靠地互相通信——即便每一部分仍可能正常运行。
在复制系统中,节点不断交换消息:写入、确认、心跳、领导者选举、读取请求。分区就是这些消息停止到达(或到达得太晚),从而导致对现实的分歧:“这个写入发生了吗?”“谁是领导者?”“节点 B 还活着吗?”
通信可以以混乱、部分化的方式失败:
重要的一点:分区通常是降级而非干净的开/关。从应用角度看,“足够慢”与“宕机”往往无法区分。
随着你增加更多机器、更多网络、更多地域和更多移动部件,通信暂时中断的机会就更多。即便单个组件可靠,整体系统也会因为依赖更多东西而出现故障。
你不需要假设具体的故障率来接受现实:只要系统运行足够久并跨足够多的基础设施,分区就会发生。
容忍分区意味着你的系统被设计成在分裂期间仍然运行——即便节点无法达成一致或无法确认对方看到的内容。这就迫使你做出选择:要么继续服务(冒不一致的风险),要么停止/拒绝某些请求(保留一致性)。
一旦你有了复制,分区就只是通信中断:系统的两个部分在一段时间内无法可靠通信。副本仍在运行,用户仍在点击,服务仍在接收请求——但副本不能就最新真相达成一致。
这就是 CAP 张力的一句话表述:在分区期间,你必须选择优先保证一致性(C)还是可用性(A)。你不能同时获得两者。
你的意思是:“我宁可正确也不愿响应。”当系统无法确认某个请求会让所有副本保持同步时,它必须失败或等待。
实际效果:一些用户会看到错误、超时或“请重试”的提示——尤其是对会更改数据的操作。这在你宁可拒绝一次支付也不愿重复扣款,或宁可阻止座位预订也不愿超卖时很常见。
你的意思是:“我宁可响应也不愿阻塞。”分区的各方会继续接受请求,即便无法协调。
实际效果:用户得到成功的响应,但读取的数据可能是旧的,并且并发更新可能产生冲突。你需要在事后依靠调和(合并规则、最后写入胜出、人工审核等)。
这不一定是一个全局设置。很多产品会混合策略:
关键时刻是按操作决定:现在阻塞用户更糟,还是以后修复冲突更糟?
“选两个”这句口号容易记,但常常误导人们以为 CAP 是个三项特征的菜单,你只能永远保留两项。CAP 讨论的是网络不配合时会发生什么:在分区(或任何类似分区的场景)期间,分布式系统必须在返回一致答案和对每个请求保持可用之间做出选择。
在真实系统中,分区不是可以关闭的设置。如果你的系统跨机器、机架、可用区或地域运行,消息可能会延迟、丢失、重排序或路由异常。从软件的角度看,这就是分区:节点无法可靠地就发生了什么达成一致。
即便物理网络没问题,其他故障也会产生相同效果——过载节点、GC 暂停、嘈杂的邻居、DNS 障碍、负载均衡器不稳定。结果相同:系统的部分无法足够好地协调。
应用不会把“分区”当成一个整洁的二元事件来体验。它们体验到的是延迟突增与超时。如果一个请求在 200 ms 后超时,不管数据包是否在 201 ms 到达,应用都必须决定下一步怎么做:从应用角度看,慢往往与坏并无二致。
很多真实系统在不同配置和运行条件下会表现为大多数时间一致或大多数时间可用。超时、重试策略、仲裁大小和“读到自己的写”选项都能影响表现。
在正常条件下,数据库可能看起来强一致;在跨区故障或压力下,它可能开始拒绝请求(倾向一致性)或返回旧数据(倾向可用性)。
CAP 更多是帮助理解当分歧发生时你所作的权衡——尤其是当分歧由普通的“变慢”引起时。
CAP 讨论常把一致性描述为二元:要么“完美”,要么“什么都行”。真实系统提供一系列保证,每种在副本分歧或网络断链时都带来不同的用户体验。
强一致性(通常指“线性化”行为)意味着一旦写入被确认,之后的任何读取——无论命中哪个副本——都会返回该写入。
代价:在分区或有少数副本不可达时,系统可能延迟或拒绝读写以避免冲突。用户会感知到超时、“请重试”或临时只读行为。
最终一致性 保证如果没有新的更新,所有副本最终会收敛。它并不保证两个同时读取的用户会看到相同的结果。
用户可能注意到:刚更新的头像“回退”,计数器滞后,或刚发出的消息在另一台设备上短时间内不可见。
你常常可以在不要求完全强一致性的前提下,获得更好的用户体验:
这些保证更贴近人的直觉(“别让我看不到自己的改动”),并且在部分故障下更容易维护。
从用户承诺开始,而不是行话:
一致性是产品选择:先定义用户眼中什么算“错误”,然后选择能防止这种错误的最弱保证。
CAP 中的可用性并不是用来吹嘘的“可用率”指标(如五个九)——而是你对用户在系统不确定时会发生什么所作的承诺。
当副本无法达成一致时,你常常要在下列之间选择:
用户会感受到“应用可用”与“应用正确”之间的权衡。两者都不是放之四海皆准的优劣;正确选择取决于“错误”的含义。稍旧的社交 Feed 令人恼火;过时的账户余额可能造成伤害。
在不确定时,两种常见行为会出现:
这不是纯粹的技术决策,而是策略决策。产品需要定义什么可以接受,什么绝不能猜测。
可用性很少是全有或全无的。在分区期间,你可能看到部分可用:某些区域、网络或用户组成功,而其他失败。这可以是刻意的设计(在本地副本健康的地方继续服务)或意外情况(路由不均、仲裁可达性不均)。
一个实用的折中方案是 降级模式:继续提供安全的操作,同时限制高风险操作。例如,允许浏览和搜索,但暂时禁用“转账”、“修改密码”或其他对正确性和唯一性要求高的操作。
CAP 在抽象上容易迷糊,直到你把它映射到分裂期间用户的体验:你是更愿意系统继续响应,还是更愿意停止以避免返回(或接受)冲突数据?
想象两个数据中心在无法通信时都接受订单。
如果你保持结账流程 可用,每一端可能都会卖出“最后一件”,导致超卖。对于低价值商品,这可能可接受(补货或致歉),但对于限量发售会很痛苦。
如果你选择 一致性优先,在无法全局确认库存时你可能会阻止新订单。用户会看到“请稍后再试”,但你避免了无法履行的销售。
金钱是经典的“错了代价高”领域。如果两个副本在分裂期间各自接受取款,账户可能出现透支。
系统通常在关键写入上优先考虑 一致性:如果不能确认最新余额,就拒绝或延迟操作。你会以部分可用性(临时支付失败)换取正确性、可审计性和信任。
在聊天和社交 Feed 中,用户通常能容忍小范围不一致:消息延迟几秒到达,点赞数不准确,查看量稍后更新。
在这些场景下,设计为可用通常是好的产品选择,只要你明确哪些元素是“最终一致”的,并且能把更新合并好。
“正确”的 CAP 选择取决于错误的代价:退款、法律风险、用户信任或运维混乱。决定哪些可以接受暂时的陈旧,哪些必须在分区时关闭。
一旦决定了在网络分裂时要做什么,需要一些机制把这个决定落到实处。这些模式在数据库、消息系统和 API 中频繁出现——即便产品方从不提“CAP”。
仲裁就是“多数副本同意”。如果你有 5 份数据,法定多数是 3。
通过要求读/写联系多数副本,你减少了返回过时或冲突数据的概率。例如,如果写入必须被 3 个副本确认,就更难出现两个隔离组各自接受不同“真相”的情况。
代价是速度与可达性:如果你无法联系多数副本(因分区或故障),系统可能拒绝操作——在这种情况下选择了一致性而非可用性。
许多“可用性”问题不是硬失败而是响应缓慢。设置较短的超时会让系统显得更灵敏,但也增加了把慢成功视为失败的概率。
重试可以从瞬时故障中恢复,但激进的重试会在服务本已吃紧时造成更大负载。退避(重试间隔逐步增长)和抖动(随机化)能防止重试引起流量峰值。
关键是把这些设置与承诺对齐:承诺“始终响应”通常意味着更多的重试与降级;承诺“绝不撒谎”通常意味着更严格的限制与清晰的错误。
如果你选择在分区期间保持可用,副本可能接受不同更新,事后必须合并。常见方法包括:
重试可能产生重复效果:重复收费或重复提交订单。幂等性可以防止这种情况。
常见模式是使用 幂等密钥(请求 ID)随请求一起发送。服务器保存第一次结果并对重复请求返回相同结果——这样重试能提高可用性而不破坏数据。
多数团队在白板上“选择”了 CAP 策略——然后在生产压力下发现系统行为不一致。验证意味着主动制造分歧场景并检查系统是否按设计反应。
你不需要真的砍断光缆就能学到东西。在预生产(并在生产中谨慎)使用故障注入来模拟分区:
目标是回答具体问题:写入会被拒绝还是接受?读取会返回旧数据吗?系统会自动恢复吗,合并需要多长时间?
如果你想尽早验证这些行为(在还没把周边服务接好之前),快速搭建一个现实的原型很有帮助。例如,团队使用 Koder.ai 时,通常会快速生成一个小服务(常见为 Go 后端 + PostgreSQL 加上 React 前端),并在沙箱环境中迭代重试、幂等密钥和“降级模式”流程以验证行为。
传统的可用性检查捕捉不到“可用但错误”的行为。要跟踪:
运维需要预先决定分区发生时的操作:何时冻结写入、何时切换、何时降级功能、以及如何验证重新合并的安全性。
同时也要计划面向用户的行为说明。如果你选择一致性,提示可能是“我们目前无法确认你的更新—请重试。”如果你选择可用性,要明确告知:“你的更新可能需要几分钟才能在所有地方生效。”清晰的文案能减少支持负担并维护信任。
在做系统决策时,CAP 最有用的就是一个快速的“分区发生时会坏什么?”审计工具——而不是理论辩论。在选数据库特性、缓存策略或复制模式前,用下面的清单快速自查。
按顺序问自己:
如果发生网络分区,你就在决定先保护哪一项。
避免使用诸如“我们是 AP 系统”的全局声明。改为按:
示例:在分区期间,你可以阻止 payments 的写入(优先一致性),但让 product_catalog 的读取通过缓存继续可用。
用例子写下你能容忍的情况:
如果你不能用简单例子描述不一致,就很难测试并解释事故。
配套的进一步主题包括:一致性共识机制(/blog/consensus-vs-cap)、一致性模型详解(/blog/consistency-models-explained)以及 SLO/错误预算(/blog/sre-slos-error-budgets)。
CAP 是用于 复制系统在通信故障下 的思维模型。当网络变慢、丢包或分裂时,副本之间无法可靠达成一致,你必须在下面两者之间做出选择:
它把“分布式系统很难”这种模糊问题,转化为具体的产品与工程决策。
真正的 CAP 场景要求同时存在:
如果你的系统是单节点或不复制状态,CAP 的权衡就不是核心问题。
分区是指系统的部分不能在可靠或在要求的时间范围内进行通信——即便每台机器仍在运行。
实际上,“分区”通常以以下形式出现:
从应用的角度看,“太慢”往往等同于“宕机”。
一致性(C) 意味着读取会反映任何已被确认的最新写入。用户体验是“我改了,大家都能看到”。
可用性(A) 意味着每个请求都会得到成功响应(不一定是最新数据)。用户体验是“应用继续工作”,但可能看到过时的结果。
在分区期间,通常无法同时对所有操作保证两者。
因为在跨机器、跨机架、跨可用区或跨地域运行的分布式系统中,分区不是可选项。只要存在复制,就必须定义在节点无法协调时系统的行为。
因此,“容忍分区”通常意味着:当通信中断时,系统仍会有明确的运行方式——要么拒绝/暂停某些操作(偏向一致性),要么提供尽力而为的结果(偏向可用性)。
如果你偏向 一致性,通常会:
这类策略常见于资金划转、库存保留与权限变更等场景——在这些地方出错比短暂不可用更糟糕。
如果你偏向 可用性,通常会:
用户会少见到硬错误,但可能看到过时数据、重复效果(如果没有幂等性)或需要清理的冲突。
可以对不同端点或数据类型做不同选择。常见的混合策略包括:
这避免了用单一“我们是 AP/CP”标签来错误地描述产品行为。
有用的保证选项包括:
通过制造分歧可见的条件来验证:
还要准备运行手册与对用户的沟通文案,使之与选定行为(fail closed vs fail open)一致。
挑选能防止用户可见“错误”的最弱保证即可。