一致性模型:从强一致到最终一致
前置知识: [../patterns/consistency/00-problem-space.md] 难度: ⭐⭐⭐⭐ 预计时间: 55分钟 关键词: 一致性模型, 线性一致性, 顺序一致性, 因果一致性, 最终一致性, CAP
引入场景
你在电商平台修改了商品价格从100改为200。刷新页面,看到还是100。再刷一次,变成200了。又刷一次,居然又回到100了。
是系统有Bug吗?不一定。这可能只是系统使用了较弱的一致性模型——不同请求可能被路由到不同的副本,而副本之间的数据同步有延迟。
理解一致性模型,就能理解这种行为为什么会发生、什么时候可以接受、什么时候不能接受。
对话探索
问题1:什么是”一致性”?在不同的上下文中它意味着什么?
💭 思考方向:
- ACID中的C(Consistency)
- CAP中的C(Consistency)
- 缓存一致性(Cache Coherence)
- 它们是同一个概念吗?
📖 参考答案:
“一致性”这个词在计算机中被不同领域复用,含义不同:
| 上下文 | 含义 | 关注什么 |
|---|---|---|
| ACID的C | 事务保持数据库约束 | 余额不能为负、外键不能悬空 |
| CAP的C | 所有副本看到的数据相同 | 读到的是最新写入的值 |
| 缓存一致性 | CPU多核缓存和内存的数据一致 | 一个核写入的值另一个核能看到 |
| 复制一致性 | 多个数据库副本的数据一致 | 主从之间数据同步 |
本文讨论的是CAP意义上的一致性——多个副本之间,读操作能否看到最新的写入。
问题2:一致性模型有哪些?它们之间是什么关系?
💭 思考方向:
- 有没有一个”最强”的一致性?
- 较弱的一致性有什么好处?
- 它们形成一个谱系?
📖 参考答案:
一致性模型谱系(从强到弱):
强 ─────────────────────────────────────── 弱
│ │
│ 线性一致性 顺序一致性 因果一致性 最终一致性 │
│ Lineariz. Sequential Causal Eventual │
│ │
│ ← 更安全,更慢,更难实现 │
│ → 更快,更可用,更容易水平扩展 → │
| 模型 | 保证什么 | 不保证什么 | 例子 |
|---|---|---|---|
| 线性一致性 | 读总是返回最新值,全局时间顺序 | - | 单机数据库 |
| 顺序一致性 | 所有操作有一个全局顺序 | 不保证实时性 | ZooKeeper |
| 因果一致性 | 因果相关的操作保持顺序 | 不相关操作可以乱序 | - |
| 最终一致性 | 如果停止写入,所有副本最终达成一致 | 不保证何时、中间状态如何 | DNS, Redis主从 |
问题3:线性一致性(Linearizability)到底意味着什么?用例子说明。
💭 思考方向:
- “好像只有一份数据”
- 写入后立即可读
- 全局时间线
📖 参考答案:
线性一致性 = 好像只有一份数据,每次操作都是瞬间完成的。
客户端A写 x=1 客户端B写 x=2 客户端C读 x
───┤ t1 ├────── ────┤ t2 ├────── ────┤ t3 ├──
写1完成后t2时刻写2
线性一致性要求:
在t3(t2之后)时刻,C读到的必须是2(最新值)
如果C读到1 → 违反线性一致性
更精确的例子:
时间 →→→→→→→→→→→→→→→→→→→→→→
A: ──[写x=1]──────────────────
B: ────────[写x=2]────────────
C: ──────────────[读x]→得到?──
线性一致性:C必须读到2(因为B的写在C的读之前完成)
顺序一致性:C可能读到1或2(只要所有客户端看到一致的顺序)
最终一致性:C可能读到旧值、新值甚至中间值
线性一致性的代价:
- 通常需要同步复制或共识协议(Raft/Paxos)
- 延迟高:写操作需要等待多数节点确认
- 可用性低:少数节点不可用时可能无法服务
问题4:最终一致性在实践中有多”最终”?是几秒还是几小时?
💭 思考方向:
- “最终”是一个很模糊的保证
- DNS的TTL可能是几小时
- Redis主从复制通常是几毫秒
- 业务能接受的延迟是多少?
📖 参考答案:
“最终”的实际时间因系统而异:
| 系统 | 最终一致的延迟 | 可接受? |
|---|---|---|
| Redis主从 | 毫秒级 | 大多数场景OK |
| MySQL主从 | 毫秒~秒级 | 读写分离注意 |
| DNS更新 | 分钟~小时 | DNS就是这么设计的 |
| CDN缓存 | 分钟~小时 | 用主动清除加速 |
| 跨区域复制 | 秒~分钟 | 跨区域天然有延迟 |
最终一致性的问题不是”有多慢”,而是”中间状态是什么”:
时间线:
t0: 数据 x=1(所有副本一致)
t1: 写 x=2(只有主副本更新了)
t2: 读副本A → x=2(已同步)
t3: 读副本B → x=1(未同步)← 不一致读
t4: 读副本A → x=1(?!回退了?)← 有些系统甚至会这样
t5: 所有副本同步完成 → x=2
t4的问题更严重:
- 如果用户先看到新值再看到旧值 → “时光倒流”
- 这叫没有单调读保证(Not Monotonic Reads)
- 有些最终一致性系统连单调读都不保证
增强版最终一致性:
最终一致性
│
├─ + 单调读(Monotonic Reads)
│ "一旦读到新值,不会再读到旧值"
│
├─ + 读你所写(Read Your Writes)
│ "能立即读到自己刚写的值"
│
├─ + 一致前缀读(Consistent Prefix Reads)
│ "按因果顺序看到数据"
│
└─ = 因果一致性(Causal Consistency)
提供了很好的平衡
问题5:CAP定理的真正含义是什么?我们真的必须”三选二”吗?
💭 思考方向:
- C = Consistency, A = Availability, P = Partition tolerance
- 网络分区是必然会发生的
- 所以实际是C和A二选一?
- 但好像很多系统同时做到了?
📖 参考答案:
CAP定理:在网络分区(P)发生时,系统必须在一致性(C)和可用性(A)之间选择。
正常情况(无分区):可以同时满足C和A
网络分区时:必须选择
选择C(一致性): 选择A(可用性):
拒绝服务直到分区恢复 继续服务但可能返回旧数据
用户看到错误/超时 用户看到(可能过时的)数据
例:银行转账 例:社交媒体动态
CAP的常见误解:
❌ “三选二” — 不准确,P不是可选的(网络分区必然发生) ❌ “CA系统” — 在分布式系统中不存在(单机可以算CA,但那不是分布式) ❌ “AP就不一致” — AP系统在正常情况下也可以很一致,只是分区时降级
更准确的理解:
平时(99.99%的时间):系统同时提供C和A
网络分区时(0.01%的时间):选择CP或AP
CP系统:分区时拒绝服务 → 恢复后数据一致(Etcd, ZooKeeper)
AP系统:分区时继续服务 → 分区恢复后需要合并冲突数据(Cassandra, DynamoDB)
PACELC模型(CAP的扩展):
- Partition时选CA;
- Else(正常情况)选Latency还是Consistency
正常情况下也有权衡:
低延迟(异步复制)↔ 强一致(同步复制)
MySQL主从(异步复制):低延迟,但从库可能滞后
Raft(同步多数确认):强一致,但写延迟更高
实际应用
如何为你的系统选择一致性模型
你的数据是?
│
├─ 金融/支付/库存
│ └─ 需要强一致性 → 关系数据库事务 / Raft协议
│
├─ 用户配置/社交内容
│ └─ 最终一致性OK → 异步复制 + 读你所写
│
├─ 搜索索引/推荐
│ └─ 最终一致性OK → 定期同步即可
│
└─ 缓存层
└─ 最终一致性 → TTL + 延迟双删
一致性与我们学过的模式的关联
| patterns中的模式 | 本质上的一致性选择 |
|---|---|
| Cache-Aside + TTL | 最终一致性(TTL时间窗口) |
| 延迟双删 | 缩短最终一致性的窗口 |
| 分布式锁(Redis) | 弱一致性(异步复制可能丢锁) |
| 分布式锁(Etcd) | 线性一致性(Raft保证) |
| 幂等性设计 | 使最终一致性系统可以安全重试 |
小结
⭐ 核心要点:
- 一致性是一个谱系:从线性一致性(最强)到最终一致性(最弱),越强越慢
- CAP的实际含义:网络分区时必须在”一致”和”可用”间选择,正常情况下几乎可以兼顾
- 没有最好的一致性模型:关键看业务需求——金融用强一致性,社交用最终一致性
关联阅读
- 基础:缓存一致性问题(具体的一致性问题)
- 模式:Cache-Aside(最终一致性的实现)
- 模式:分布式锁(不同一致性级别的锁)
- 深入:抽象层次(一致性模型是对”数据同步”的抽象)
- 深入:系统思维(一致性vs性能的权衡思维)