分布式系统里,"一致性"大概是被说得最多、也最容易被误解的词。这篇文章从用户体验和系统约束两个角度聊聊这个概念,以及为什么网络一出问题,一致性和可用性就只能二选一。
一、什么是"一致性"?
一个扎心的场景
你发了一条朋友圈,刷新后发现——没了。
再刷新,又出现了。反复横跳,像极了你的感情生活。
这就是一致性问题的日常体感。你的数据写进去了,但读的时候,系统不认账。
一致性,说到底就是一句话:系统对外表现得像只有一份数据。
但这句话背后,藏着两个完全不同的视角。
用户视角:我只关心我的体验
作为用户,你不关心系统内部有几个副本、数据怎么同步。你只关心:
读己之写(Read Your Writes)
"我刚写的东西,我必须能读到。"
---
config:
theme: forest
look: neo
---
sequenceDiagram
participant User as 👤 用户
participant Node1 as 🖥️ 节点A
participant Node2 as 🖥️ 节点B
User->>Node1: 写入:"头像=猫猫.jpg"
Node1-->>User: 写入成功 ✓
Note over Node1,Node2: ☁️ 同步可能有延迟...
User->>Node2: 读取头像
Node2-->>User: 头像=旧图.jpg ✗
Note right of User: 🧐 用户:???我刚改的呢?
这是最基本的用户预期。如果连这个都做不到,用户会觉得你的系统"有毛病"。
单调读(Monotonic Reads)
"时间不能倒流,我看到的数据不能越来越旧。"
---
config:
theme: forest
look: neo
---
sequenceDiagram
participant User as 👤 用户
participant Node1 as 🖥️ 节点A(已同步)
participant Node2 as 🖥️ 节点B(未同步)
User->>Node1: 第一次读取
Node1-->>User: 余额=100元(最新)
User->>Node2: 第二次读取
Node2-->>User: 余额=80元(旧数据)
Note right of User: 🧐 用户:我的20块钱呢?
用户第一次看到余额 100,第二次看到 80,会以为钱被盗了。实际上只是读到了不同副本的数据。
单调读要求:一旦读到某个值,后续读取只能看到相同或更新的值,不能倒退。
系统视角:全局时间线的约束
用户体验的背后,是系统工程师的噩梦。让我们换个视角,看看系统内部在纠结什么。
线性一致性 —— 最强的一致性
一句话定义:所有操作看起来是原子化的,且严格遵守一个全局同步的时间线。
graph LR
subgraph Time [物理时间轴]
direction LR
A[T1: 客户端A写入 x=1] --> B[T2: 客户端B读取 x]
Start((开始)) --- A
B --> C[T3: 客户端A写入 x=2]
C --> D[T4: 客户端C读取 x]
D --- End((结束))
end
style A fill:#90EE90
style C fill:#90EE90
style B fill:#87CEEB
style D fill:#87CEEB
style Time fill:#fdfdfd,stroke:#ddd,stroke-dasharray: 5 5
线性一致性要求:
- T2 时刻读取,必须返回
x=1(因为写入已完成) - T4 时刻读取,必须返回
x=2(因为新写入已完成) - 任何操作一旦完成,所有后续操作都必须看到它
代价:每次操作都要和"全局真相"同步,性能很差。
顺序一致性 —— 稍弱一点
一句话定义:每个客户端内部的操作顺序保持不变,不同客户端之间的操作可以重排。
举个例子:
- 客户端 A 执行:先写 x=1,再写 y=2
- 客户端 B 执行:先读 y,再读 x
假设物理时间上,A 的 写y=2 在 B 的 读y 之前就已经返回了。那么:
-
线性一致性:B 读 y 必须返回 2(必须尊重物理时间)
-
顺序一致性:B 读 y 返回 0 也行,只要 A 内部的顺序(先写x后写y)和 B 内部的顺序(先读y后读x)都保持不变。
实际场景例子:
- 线性一致性:你发完微信消息,对方立刻能看到
- 顺序一致性:你发的多条消息,对方看到的顺序和你发送的顺序一致,但可能有延迟
因果一致性 —— 只管有因果关系的操作
一句话定义:有因果关系的操作必须按顺序被所有客户端看到,没有因果关系的操作顺序随意。
什么叫"有因果关系"?
- A 写了 x=1;B 读到了 x=1,然后 B 写了 y=2 —— 这里
写x=1和写y=2就有因果关系 - A 写了 x=1,C 写了 z=3,两者互不相干 —— 没有因果关系
举个例子:
A: 写 x=1
B: 读 x=1,然后写 y=2
C: 观察 x 和 y
- 因果一致性要求:C 如果看到了
y=2,就必须也能看到x=1(因为 y=2 依赖于 x=1) - 但不要求:C 必须先看到 x=1 再看到 y=2,只要最终都能看到就行
和顺序一致性的区别:顺序一致性要求所有客户端看到的全局顺序一致,因果一致性只要求有因果关系的那部分顺序一致,其他的可以不一致。
最终一致性 —— 最弱但最实用
一句话定义:如果没有新的写入,最终所有客户端都会读到相同的值。
注意这个"最终"——没有时间承诺,可能是 1 秒,也可能是 1 小时。
举个例子:
你改了头像,你朋友刷新后还是旧头像,过了几秒再刷才看到新的。这就是最终一致性。
为什么要用这么弱的一致性? 因为快、可用性高、实现简单。对于很多业务场景(朋友圈、商品评论、DNS),"晚几秒看到"根本不是问题。
一致性强度谱系图
graph LR
subgraph ConsistencyGroup ["一致性强度:由强到弱"]
L["线性一致性<br/>Linearizability"] --> S["顺序一致性<br/>Sequential"]
S --> C["因果一致性<br/>Causal"]
C --> E["最终一致性<br/>Eventual"]
end
L -.- L1(["单机数据库"])
S -.- S1(["ZooKeeper"])
C -.- C1(["部分 NoSQL"])
E -.- E1(["DNS / CDN"])
%% 主流程样式
style L fill:#ff6b6b,color:#fff,stroke:#e66767
style S fill:#ffa502,color:#fff,stroke:#eccc68
style C fill:#2ed573,color:#fff,stroke:#7bed9f
style E fill:#1e90ff,color:#fff,stroke:#70a1ff
%% 实例节点样式 (柔和色系)
style L1 fill:#fff5f5,stroke:#ff6b6b,stroke-width:1px
style S1 fill:#fff9f0,stroke:#ffa502,stroke-width:1px
style C1 fill:#f0fff4,stroke:#2ed573,stroke-width:1px
style E1 fill:#f0f7ff,stroke:#1e90ff,stroke-width:1px
%% 背景颜色
style ConsistencyGroup fill:#fdfdfd,stroke:#ddd,stroke-dasharray: 5 5
区别总结:
| 特性 | 线性一致性 | 顺序一致性 | 因果一致性 | 最终一致性 |
|---|---|---|---|---|
| 物理时间 | 必须尊重 | 可以忽略 | 可以忽略 | 可以忽略 |
| 全局顺序 | 唯一且实时 | 唯一但可重排 | 只保证因果链 | 不保证 |
| 性能 | 最差 | 较差 | 较好 | 最好 |
| 典型场景 | 分布式锁 | 多副本数据库 | 协作编辑 | DNS / CDN |
选择原则:没有最好的一致性,只有最适合业务场景的一致性。
二、物理世界的枷锁
看完上一节,你可能会想:既然线性一致性最强,那我全都要线性一致性不就完了?
很遗憾,物理世界不允许。
FLP 不可能性定理:异步网络的诅咒
1985 年,Fischer、Lynch 和 Paterson 三位大神证明了一个令人绝望的结论:
在异步网络中,只要有一个节点可能崩溃,就无法在有限时间内让所有节点对某个值达成一致。
为什么?问题出在"异步网络"这四个字上。
graph LR
classDef base fill:#e3f2fd,stroke:none,color:#1565c0;
classDef highlight fill:#fff8e1,stroke:none,color:#f57f17;
classDef critical fill:#ffcdd2,stroke:none,color:#b71c1c;
N1[节点1]:::base -->|发送消息| N2[节点2]:::base
N2 -.->|"迟迟没有回应..."| N1
N1 --> Q{"节点2 怎么了?"}:::highlight
Q --"消息还在路上?"--> R[无法区分]:::critical
Q --"节点2 挂了?"--> R
linkStyle default stroke:#90a4ae,stroke-width:2px;
linkStyle 1 stroke:#e57373,stroke-width:2px,color:#e57373,stroke-dasharray: 5 5;
- 如果你选择等:可能等到天荒地老
- 如果你选择不等:可能误判(对方其实没挂)
这就是 FLP 的核心结论:在异步网络中,你永远无法在有限时间内确定对方的状态,因此无法保证所有节点达成一致。
FLP 告诉我们:完美的一致性在理论上不可能,工程上只能做到"足够好"。怎么做到?后面讲 Paxos、Raft 时会详细展开。
CAP 定理:三选二的永恒困境
FLP 说的是"达成一致很难",CAP 说的是"就算能达成一致,你也得做选择"。
2000 年,Eric Brewer 提出了著名的 CAP 定理:
一个分布式系统,最多只能同时满足以下三项中的两项。
这三项是:
| 字母 | 含义 | 解释 |
|---|---|---|
| C | Consistency(一致性) | 所有节点看到的数据相同 |
| A | Availability(可用性) | 每个请求都能得到响应(不会被拒绝或超时) |
| P | Partition Tolerance(分区容错) | 网络分区(节点之间失联)时系统仍能运行 |
为什么不能三个都要?
因为在分布式系统中,网络分区(P)一定会发生——光缆被挖断、交换机故障、机房断网……
分区发生时,节点之间无法通信,数据没法同步。这时候你只有两个选择:
- 保 C 舍 A:拒绝服务,等网络恢复、数据同步完再响应。数据是一致的,但用户等不到响应。
- 保 A 舍 C:继续响应,但返回的可能是旧数据。用户有响应,但数据可能不一致。
P 是前提,不是选项。CAP 的本质是:当网络分区发生时,你选 C 还是选 A?
mermaid
---
config:
theme: forest
look: neo
---
sequenceDiagram
participant Client as 客户端
participant Node1 as 节点1
participant Node2 as 节点2
Note over Node1,Node2: 网络分区!节点间无法通信
Client->>Node1: 写入 x=1
Node1-->>Client: 写入成功
rect rgb(255, 200, 200)
Note over Node1,Node2: 数据无法同步到节点2
end
Client->>Node2: 读取 x
alt 选择 C(一致性)
Node2->>Client: 拒绝服务
Note right of Client: 宁可不可用,也不返回旧数据
else 选择 A(可用性)
Node2->>Client: x=旧值
Note right of Client: 宁可返回旧数据,也要响应
end
常见系统的选择
| 系统 | 选择 | 理由 |
|---|---|---|
| ZooKeeper / Etcd | CP | 配置中心、分布式锁,数据必须一致 |
| Redis Cluster / MongoDB | AP | 高可用优先,允许短暂的数据不一致 |
CAP 不是一次性的架构决策,而是每次网络分区时的实时抉择。
三、两条路线的选择
理论讲完了,我们来看看工程实践中,两大阵营是怎么落地的。
ACID:数据库人的执念
传统数据库出身的架构师,骨子里信奉一个字:稳。
ACID 是数据库事务的四个特性:
| 特性 | 含义 | 白话翻译 |
|---|---|---|
| 原子性(Atomicity) | 事务要么全成功,要么全失败 | 转账不能钱扣了对面没收到 |
| 一致性(Consistency) | 事务前后数据库状态都合法 | 不能出现负库存 |
| 隔离性(Isolation) | 并发事务互不干扰 | 你读的时候我改不影响你 |
| 持久性(Durability) | 提交后数据不丢失 | 断电重启数据还在 |
ACID 的核心信条:宁可慢,不能错。
典型代表:MySQL、PostgreSQL、Oracle
适用场景:金融交易、库存扣减、订单状态——任何"错一条都是生产事故"的场景。
⚠️ ACID 的 C 和 CAP 的 C 不是一回事!
- ACID 的 C(一致性):数据库约束不被破坏,比如转账前后总金额不变
- CAP 的 C(一致性):分布式副本之间数据相同,即线性一致性
BASE:互联网人的妥协
互联网业务的特点是什么?量大、高并发、允许短暂不一致。
于是 eBay 的架构师们提出了 BASE:
| 特性 | 含义 | 白话翻译 |
|---|---|---|
| 基本可用(Basically Available) | 出故障时保证核心功能可用 | 双十一退货功能可以挂,但下单必须能用 |
| 软状态(Soft State) | 数据副本之间允许暂时不同步 | 你在节点A改了数据,节点B可以晚几秒才同步到 |
| 最终一致(Eventually Consistent) | 经过一段时间后达成一致 | 不保证立刻一致,但最终会一致 |
BASE 的核心信条:先活下来,再慢慢对。
典型代表:Redis Cluster、MongoDB、消息队列
适用场景:朋友圈、商品评论、用户行为日志——"晚一秒看到问题不大"的场景。
ACID vs BASE:不是对立,是互补
ACID 和 BASE 不是非此即彼的选择,而是根据业务场景灵活组合。
核心问题就一个:数据错了,后果有多严重?
| 业务场景 | 一致性要求 | 推荐思路 |
|---|---|---|
| 用户余额扣减 | 强一致 | ACID 事务 |
| 订单创建 + 库存扣减 | 最终一致 | 先创建订单,异步扣库存 |
| 用户头像更新 | 最终一致 | 异步更新,允许短暂不一致 |
| 日志采集 | 弱一致 | 允许少量丢失 |
一句话总结:钱相关的用 ACID,体验相关的用 BASE。
总结
mindmap
root((一致性图谱))
什么是一致性
用户视角
读己之写
单调读
系统视角
线性一致性
顺序一致性
因果一致性
最终一致性
物理限制
FLP 不可能性
异步网络无法保证一致
CAP 定理
P 是前提
C vs A 的抉择
工程路线
ACID
强一致
宁慢不错
BASE
最终一致
先活再对
三句话总结:
-
一致性的本质:让分布式系统表现得像单机一样,但代价各不相同
-
物理定律的限制:FLP 说完美不可能,CAP 说分区时必须在 C 和 A 之间选择
-
工程的智慧:ACID 和 BASE 不是非此即彼,而是根据业务场景灵活组合
下篇预告
这篇讲了一致性的"是什么"和"为什么难",下一篇我们看看前人是怎么尝试解决这个问题的。
最直观的思路是:让所有节点投票,全票通过才提交。这就是 2PC(两阶段提交)。
听起来很美好,但有个致命问题……
下一篇《原子提交的演进 —— 2PC、3PC 与网络分区的难题》,我们来看看这条路为什么走不通。