分布式一致性:CAP、Raft 与共识
分布式系统绕不开一致性——CAP 怎么取舍、Raft 怎么达成共识、最终一致 vs 强一致怎么选。本文用大白话讲清 CAP 定理的真相(不是三选二)、Raft 选举与日志复制、以及实际系统(主从/Quorum/CRDT)怎么在不同一致性间取舍。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
分布式一致性:CAP、Raft 与共识
先抛一个经典线上事故:
机房之间光纤抖动,主库和备库之间的网络断了 30 秒。这 30 秒里,两台机器都认为自己是"主",各自接收了写入。等网络恢复,工程师面对的是两份互相冲突的数据——这就是教科书级的脑裂(split-brain)。
只要你做分布式,迟早会撞上这类问题。多台机器组成一个系统,网络会断、机器会挂、时钟会漂——在这种不可靠的底座上,怎么让数据"看起来像只有一份"?这就是**一致性(consistency)和共识(consensus)**要解决的事。
这篇文章讲透四件事:CAP 定理到底在说什么(以及大众怎么误读它)、一致性的强度谱系、Raft 怎么用大白话达成共识、以及工程上主从/Quorum/CRDT 到底怎么取舍。
一、一致性为什么绕不开
单机数据库不需要操心一致性——一份写在磁盘上,读出来就是它。但分布式系统天然是多副本:为了高可用(一台挂了另一台顶)、为了低延迟(数据就近部署)、为了读吞吐(多副本并行读)。
只要数据被复制到多台机器,问题就来了:
- 写一台之后,另一台什么时候能读到?立刻,还是延迟几秒?
- 两台同时收到不同的写,以谁为准?
- 网络断了的时候,两边各自写,合起来怎么处理?
副本是分布式一切问题的根源。一致性,就是回答"多个副本在任意时刻,对外能不能表现得像一个整体"。
工程上不存在"完美的一致性"——那等于单机。我们要做的,是在"一致性强一点"和"系统可用一点 / 性能好一点"之间做取舍。而这个取舍的理论天花板,就是 CAP。
二、CAP 定理的真相:别再"三选二"了
坊间最常见的误读:"CAP 就是 consistency、availability、partition tolerance 三选二"。于是有人拍胸脯说"我们系统是 CA"。这话基本是错的。
CAP 的准确表述是:在网络发生分区(partition)的时候,你只能在一致性和可用性之间二选一。
- C(Consistency):读到的一定是最新写入的。
- A(Availability):每个请求都能收到(非错误的)响应,不保证是最新。
- P(Partition Tolerance):网络允许丢消息、分区、延迟。
关键认知:P 不是可选的,是物理事实。网络一定会分区,交换机会抖、光纤会被挖断、机器网卡会过载。分布式系统不能假装 P 不存在。
所以真正的取舍是这样的:
没有发生分区(正常情况):C 和 A 可以同时要,大家开心。
发生分区(网络断了): 要么拒绝写(C,牺牲 A),
要么允许各写各的(A,牺牲 C)。
CAP 应该被理解为"分区时的一致性/可用性权衡",而不是"三个里挑两个"。不存在真正意义上的 CA 分布式系统——除非你是单机。
工程上:
- 多数传统关系库分库中间件:分区时保 C,拒绝脑裂那一边的写。
- Cassandra、Dynamo 这类系统:分区时保 A,先让写进去,事后靠向量时钟、读修复等手段最终一致。
- 绝大多数现代系统(ZooKeeper、etcd、consul)走的是 CP 路线——共识协议保证强一致,牺牲少数派的可用性。
一句话:CAP 不是用来选型的,是用来理解系统在故障下的极限行为的。
三、一致性谱系:不是非黑即白
CAP 里的 C 是个二元概念,但真实世界的一致性是连续光谱,从强到弱大致四档:
| 强度 | 通俗解释 | 代表 |
|---|---|---|
| 线性一致(Linearizability) | 全局只有一份"当前值",读写像单机一样严格 | etcd、Spanner |
| 顺序一致(Sequential) | 所有节点看到的操作顺序一致,但不一定与真实时间对应 | ZooKeeper 的写 |
| 因果一致(Causal) | 有因果关系的操作保序,无因果的可乱序 | 评论系统、协作工具 |
| 最终一致(Eventual) | 停止写入后,副本最终会收敛到一致;中间随便乱 | DNS、Cassandra、S3 |
强度越高,实现代价越大(需要协调、加锁、跨节点 RPC),延迟越高。强度越弱,性能和可用性越好,但应用层要容忍"读到旧数据"。
工程判断:绝大多数互联网业务用最终一致就够了。只有少数场景——账户余额、库存扣减、分布式锁——才需要线性一致。别一上来就要求全局强一致,那是给自己挖坑。
四、Raft:讲人话的共识协议
线性一致怎么实现?靠共识算法:让一组机器对"下一条要写入什么"达成一致。Raft 是当下最主流的共识协议,etcd、Consul、TiKV、Redis Cluster 的 gossip、Kubernetes 的状态都靠它。
Raft 之所以取代 Paxos 成为教学和工程的事实标准,核心就一句话:它为了可理解性而设计。它把共识拆成三个清晰的子问题:选主、日志复制、安全性。
1. 选主(Leader Election)
Raft 集群里同一时刻只有一个 Leader,所有写请求都走它。选主流程:
节点启动 / Leader 失联(心跳超时)
↓
term(任期)加 1,自己变成 Candidate
↓
投自己一票,并行发 RequestVote 给其他节点
↓
收到多数(N/2 + 1)票 → 当选 Leader
↓
周期性发心跳维持权威,其他节点降为 Follower
关键设计:多数决(quorum)。不需要所有节点都同意,只要多数同意即可。一个 5 节点集群,挂 2 个还能正常选主;挂 3 个就选不出 Leader,集群停服——这就是 CP 系统的代价。
2. 日志复制(Log Replication)
Leader 收到写请求后,不是写完就返回,而是走一套复制流程:
客户端 → Leader 写本地日志(未提交)
↓
Leader 并行复制到所有 Follower
↓
多数 Follower 复制成功 → Leader 标记"已提交"
↓
Leader 返回客户端成功,异步通知 Follower 提交
**为什么是多数,而不是全部?**因为"多数 + 多数"必然有交集——任意两个多数派至少有一个共同节点。这就保证了已提交的日志不会被覆盖(安全性)。如果等所有节点,那一个慢节点就会拖垮整个集群的写入。
3. 一个完整写请求的生命周期
Client → Leader: SET x=5
Leader: append 日志 [term=3, idx=7, SET x=5]
Leader → Followers: AppendEntries RPC
Followers: 写入日志,回 OK
Leader: 收到 3/5 节点(含自己)确认 → 提交(commit)
Leader: 应用到状态机(x=5),返回 Client 成功
Leader → Followers: 下一次心跳带上 commitIndex,Follower 提交
这套机制保证了:已提交的日志一定不丢、所有节点最终一致、Leader 切换时数据连续。代价是每次写都要走一次跨节点 RPC,延迟比单机高一个数量级。这就是强一致的"价格标签"。
五、Paxos:祖师爷,知道它存在就行
Raft 之前,共识算法的事实标准是 Paxos(Lamport 1998)。它在数学上极优雅,证明完备,Google Chubby、Spanner 都基于它。问题只有一个:太难懂。论文里"准备阶段"、"接受阶段"、proposal number 那一套绕得云里雾里,连工程实现都各显神通(Multi-Paxos 没有标准实现)。
Raft 的作者(Diego Ongaro)就是受了 Paxos 的折磨,刻意设计了一个"功能上等价、但更好理解"的算法。所以工程上:理解 Raft 就够了,Paxos 留给读论文和面试装 X 用。看到 Paxos 字样,知道它是 Raft 的祖宗、理论更强但工程更难,这就足够了。
六、实际系统怎么取舍
理解了原理,看实战。三种典型的工程取舍,对应三种一致性强度:
1. 主从异步复制(最终一致,可用性优先)
MySQL 半同步、Redis 主从、ES 副本,基本都属于这一类。Leader 写本地立刻返回,异步把日志推给副本。
- 优点:写延迟低,Leader 挂了虽然有少量数据丢失,但系统整体可用。
- 风险:Leader 挂掉瞬间未同步的写会丢;异步期间读到旧数据。
- 适用:日志、用户行为、缓存、搜索索引——能容忍秒级数据延迟的场景。
2. Quorum / NWR(可调一致性)
Dynamo、Cassandra、Riak 把选择权交给用户,核心是 NWR 模型:
- N:副本总数
- W:写要求多少副本成功
- R:读要求多少副本响应
只要 W + R > N,读写一定有交集,就能保证强一致。典型 N=3:
- W=2, R=2:强一致,延迟中等(主流配置)。
- W=1, R=1:最终一致,性能最高。
- W=3, R=1:写重读轻的场景。
Quorum 的精髓是"一致性可调"——同一套存储,不同业务用不同配置。这是非常工程化的设计。
3. CRDT(无冲突数据类型,最终一致无需协调)
CRDT(Conflict-free Replicated Data Type)是另一条路:数据结构本身的设计保证,无论以什么顺序合并,最终结果都一致。
典型应用:协作文档(在线文档、Notion)、Yjs、RedisCRDTs、Riak Map/Set。你和人同时改一段文字,本地直接改,后台合并,不冲突——因为数据结构(比如 G-Counter、LWW-Set)在数学上保证了收敛。
CRDT 的代价是表达能力受限(不是所有操作都能无冲突合并),但优势是完全没有协调成本,可用性拉满。协作类、社交类、IM 状态同步,首选 CRDT。
七、ACID vs BASE:两套哲学
数据库领域有 ACID(原子、一致、隔离、持久),强调事务的严格一致;分布式领域有 BASE(Basically Available、Soft state、Eventually consistent),强调放弃强一致换可用性和性能。
这俩不是对立的,而是不同层次的取舍:
- 单库事务的 ACID,保证库内数据正确。
- 跨库、跨服务的分布式事务(2PC、TCC),保证全局强一致——但又重又慢又容易错,生产里慎用。
- BASE 思路:接受中间状态软一致,用补偿、幂等、消息最终对账。
工程上主流做法是 Saga + 幂等 + 消息队列:把一个长事务拆成本地事务 + 事件,各服务独立提交,失败靠补偿回滚。这套比 2PC 实用 10 倍,是当下微服务一致性的事实标准。
八、工程上别强求全局强一致
最后说点实战判断。一致性选型有个朴素原则:按业务价值选,不按技术洁癖选。
- 钱、库存、权限:必须强一致(线性一致)。账户余额少了 1 块钱就是事故,这笔钱不能省,用 Raft / 数据库事务 / TCC。
- 用户资料、商品信息:最终一致就够。延迟一两秒看到新头像,没人会投诉。
- 社交动态、评论、点赞:最终一致 + CRDW-like 合并。用户感受不到一致性强弱,但系统能扛住巨大流量。
- 计数、排行、统计:最终一致 + 异步聚合。准确性差个 0.1% 没人在乎,吞吐差一个数量级有人在乎。
全局强一致是奢侈品:每一次强一致写,都是一次跨节点协调、一次延迟放大、一次可用性妥协。把它用在真正需要的地方,其他地方大胆用最终一致。
小结
写到这里,几条要点浓缩一下:
- 副本是分布式问题的根源,一致性就是回答"多副本怎么表现得像一份"。这不是技术洁癖,是业务正确性的底线。
- CAP 的真相是"分区时的取舍",不是三选二。P 是物理事实,正常时 C 和 A 都可以要;分区发生那一刻,才需要选 CP 还是 AP。绝大多数存储系统走 CP。
- 一致性是连续光谱:线性一致最强最贵,最终一致最弱最快。别一上来就要全局强一致,按业务价值挑档位。
- Raft 三件事:选主、日志复制、多数决。理解"多数 + 多数必有交集"这一个点,就理解了它为什么既安全又能容忍少数派故障。Paxos 是祖宗,知道它存在即可。
- 选型看钱:钱的场景上强一致,社交和内容用最终一致 + CRDT / Saga,跨服务事务用"本地事务 + 消息 + 补偿"而非 2PC。
最后送一句金句:
分布式一致性的本质,不是消除冲突,而是诚实地面对冲突——在钱的面前强一致,在流量面前最终一致,在分区面前清醒地选 CP 还是 AP。强一致是奢侈品,不是日用品。
下一篇我们聊聊"分布式事务:Saga、TCC 与本地消息表",看看跨服务怎么把一致性这碗饭吃得更稳。
Related
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。