强一致性很美好,但代价是延迟和可用性。亚马逊的 Dynamo 论文问了一个问题:如果允许短暂不一致,能换来什么?答案是:永远可用。
前情回顾
在前几篇中,我们看到了强一致性的实现方式:
- 2PC:协调者单点问题
- Paxos/Raft:多数派共识,解决单点问题
但强一致性有代价:
| 代价 | 说明 |
|---|---|
| 延迟高 | 每次写入要等多数派确认 |
| 可用性受限 | 网络分区时,少数派无法服务 |
| 跨地域困难 | 北京到美国 200ms 延迟,写一次要等 400ms+ |
问题:所有场景都需要强一致性吗?
一个购物车的故事
你在淘宝把一件商品加入购物车。
如果用强一致性:
你的手机 北京机房 上海机房
│ │ │
│─ 加入购物车 ────►│ │
│ │ │
│ │── 同步到上海 ──►│
│ │ │
│ │◄── 确认 ────────│
│ │ │
│◄─ 成功 ──────────│ │
耗时:网络延迟 × 2 ≈ 100ms
问题:如果北京到上海的网络断了呢?
- 强一致性的选择:「抱歉,购物车服务暂时不可用」
- 用户的反应:「什么破网站,加个购物车都不行?」
但其实,购物车不需要这么强的一致性:
- 加购物车成功就行,哪怕另一个机房晚几秒看到
- 极端情况下数据丢了,用户重新加一次也能接受
- 比起「服务不可用」,用户更能接受「数据延迟」
这就是最终一致性的适用场景。
最终一致性:定义
如果没有新的更新,最终所有副本会达到一致状态。
关键词:「最终」。
| 强一致性 | 最终一致性 |
|---|---|
| 写完立刻可见 | 写完可能延迟可见 |
| 所有副本同时更新 | 副本逐步同步 |
| 网络分区时可能不可用 | 网络分区时仍可服务 |
| 适合金融、库存 | 适合社交、缓存、日志 |
2007 年:Dynamo 论文
亚马逊在 2007 年发表了著名的 Dynamo 论文,描述了他们的购物车系统是如何实现「永远可用」的。
核心思想:
在 CAP 三角中,我们选择 AP(可用性 + 分区容忍)。
代价是接受「最终一致性」。
Dynamo 的设计原则
| 原则 | 说明 |
|---|---|
| 永远可写 | 即使网络分区,也能接受写入 |
| 永远可读 | 即使数据可能过时,也返回结果 |
| 冲突后处理 | 先接受写入,冲突了再想办法解决 |
| 去中心化 | 没有 Leader,所有节点平等 |
Quorum:用数学保证「够多」
Dynamo 用 Quorum 机制来平衡一致性和可用性。
基本概念
假设数据有 N 个副本:
| 参数 | 含义 |
|---|---|
| N | 副本总数 |
| W | 写入成功需要的副本数 |
| R | 读取成功需要的副本数 |
关键公式:
如果 W + R > N,读写一定有交集,能读到最新数据。
举例说明
假设 N = 3(3 个副本):
配置一:W=2, R=2
写入时:
副本 A: ✓ 写入成功
副本 B: ✓ 写入成功
副本 C: ✗ 还没同步到
读取时:
从副本 A 和 B 读取
至少一个有最新数据 ✓
W + R = 4 > N = 3,保证能读到最新值。
配置二:W=1, R=1
写入时:
副本 A: ✓ 写入成功
副本 B: 还没同步
副本 C: 还没同步
读取时:
只读副本 C
读到旧数据 ✗
W + R = 2 < N = 3,可能读到旧值。
常见配置
| 配置 | W | R | 特点 |
|---|---|---|---|
| 强一致 | N | 1 | 写慢,读快,写入要等所有副本 |
| 强一致 | 1 | N | 写快,读慢,读取要查所有副本 |
| 平衡 | 2 | 2 | (N=3) 写读都要多数派 |
| 最终一致 | 1 | 1 | 最快,但可能读到旧值 |
亚马逊的选择:对购物车,用 W=1(写入一个副本就返回成功),保证永远可写。
向量时钟:谁是最新的?
当多个副本可以独立写入时,一个问题出现了:谁的数据是最新的?
物理时钟不可靠
回忆第一篇:每台机器的时钟都不一样,不能用时间戳判断先后。
机器 A (时钟快) 机器 B (时钟慢)
│ │
10:00:05 写入 x=1 │
│ │
│ 10:00:00 写入 x=2
│ │
时间戳:A 更新(10:00:05)
实际:B 更新(发生在后面)
向量时钟的思想
给每个节点一个逻辑时钟,记录「我见过的版本」:
向量时钟格式:{节点A: 版本, 节点B: 版本, ...}
初始状态(假设两个节点 A 和 B):
副本 A: x=0, 时钟={A:0, B:0}
副本 B: x=0, 时钟={A:0, B:0}
A 写入 x=1(A 的计数器 +1):
副本 A: x=1, 时钟={A:1, B:0}
B 写入 x=2(B 的计数器 +1,但不知道 A 的更新):
副本 B: x=2, 时钟={A:0, B:1}
现在两个副本:
A: x=1, {A:1, B:0}
B: x=2, {A:0, B:1}
判断先后关系
规则:如果时钟 V1 的每个分量都 ≤ V2 的对应分量,则 V1 早于 V2。
{A:1, B:0} vs {A:2, B:1}
A分量:1 < 2 ✓
B分量:0 < 1 ✓
结论:{A:1, B:0} 早于 {A:2, B:1}
如果无法判断先后呢?那就是并发冲突。
{A:1, B:0} vs {A:0, B:1}
A分量:1 > 0
B分量:0 < 1
结论:无法判断先后,这是并发写入,产生冲突
冲突怎么办?
Dynamo 的策略:交给应用层解决。
客户端读取时:
副本 A: x=["苹果"], 时钟={A:1}
副本 B: x=["香蕉"], 时钟={B:1}
Dynamo 返回:
两个版本都返回给客户端
[["苹果"], ["香蕉"]]
应用层决定:
购物车场景:合并两个 → ["苹果", "香蕉"]
其他场景:可能选最新一个,或让用户选
购物车的冲突解决:合并所有商品。宁可多加,不能少。
读修复与反熵
数据不一致后,怎么修复?
读修复(Read Repair)
读取时顺便修复旧数据:
客户端读取 x:
│
│──► 副本 A: x=1 (最新)
│──► 副本 B: x=0 (旧)
│
│ 发现 B 的数据旧了
│
│──► 副本 B: 这是最新值 x=1,更新一下
优点:热数据自然会被修复。
缺点:冷数据可能永远不被读取,永远不被修复。
反熵(Anti-Entropy)
后台进程定期同步所有副本:
每隔 1 小时:
比较所有副本的数据
把不一致的数据同步一遍
优点:冷数据也能被修复。
缺点:消耗资源,同步间隔内数据仍可能不一致。
实际案例:你的朋友圈
微信朋友圈是最终一致性的典型应用。
场景:你发了一条朋友圈
你的手机 深圳机房 北京机房
│ │ │
│── 发朋友圈 ────────►│ │
│ │ │
│◄── 发布成功 ────────│ │
│ │ │
│ (后台异步同步) │
│ │───────────────►│
│ │ │
时间线:
0ms: 你看到自己发的朋友圈了
50ms: 深圳的朋友看到了
200ms: 北京的朋友看到了
500ms: 美国的朋友看到了
这种短暂不一致能接受吗? 能。社交场景不需要全球同时看到。
最终一致性的变种
根据「最终」的时间窗口和保证强度,有不同的一致性级别:
| 级别 | 含义 | 延迟 |
|---|---|---|
| 因果一致性 | 有因果关系的操作保序 | 毫秒级 |
| 读己之写 | 自己写的数据自己立刻能读到 | 毫秒级 |
| 会话一致性 | 同一会话内保证读己之写 | 毫秒级 |
| 单调读 | 不会读到比之前更旧的数据 | 毫秒级 |
| 最终一致 | 只保证「最终」一致 | 秒~分钟级 |
大多数应用需要的是「读己之写」:我刚发的帖子,我自己必须能立刻看到。
什么时候用最终一致性?
| 适合 | 不适合 |
|---|---|
| 社交动态 | 银行转账 |
| 购物车 | 库存扣减 |
| 用户浏览记录 | 订单创建 |
| 点赞数 | 支付 |
| 缓存 | 分布式锁 |
| 日志收集 | 配置中心 |
判断标准:
- 短暂不一致会导致金钱损失吗?
- 短暂不一致会导致用户体验严重受损吗?
- 数据冲突时,有简单的合并策略吗?
如果答案是「不会」「不会」「有」,就可以考虑最终一致性。
常见问题
Q:最终一致性的「最终」是多久?
A:没有保证,取决于系统设计。
- 正常情况:毫秒到秒级
- 网络拥塞:秒到分钟级
- 网络分区:可能是小时级,直到分区恢复
你能接受的延迟,决定了系统的设计。
Q:最终一致性会丢数据吗?
A:不一定,取决于配置。
- W=1 且节点挂了:如果数据还没同步出去,可能丢失
- W=2 且配合持久化:至少两个节点有数据,丢失概率很低
Dynamo 的选择是:宁可数据冲突,也不丢数据。冲突可以合并,丢了就没了。
Q:能不能混合使用?
A:可以,这是最佳实践。
电商系统:
库存服务:强一致性(CP)
购物车服务:最终一致性(AP)
商品评论:最终一致性(AP)
订单服务:强一致性(CP)
不同业务不同对待。
总结
最终一致性的本质:
用一致性换可用性,用「延迟一致」换「永远可用」。
Dynamo 的贡献:
| 贡献 | 说明 |
|---|---|
| Quorum 机制 | 灵活控制一致性和可用性的平衡 |
| 向量时钟 | 追踪并发,识别冲突 |
| 读修复 + 反熵 | 最终达成一致的机制 |
| 去中心化 | 没有单点故障 |
核心取舍:
| 选择 | 得到 | 失去 |
|---|---|---|
| 强一致 | 数据永远正确 | 可用性、延迟 |
| 最终一致 | 永远可用、低延迟 | 短暂不一致 |
最终一致性解决了「可用性」问题,但有一个麻烦:冲突需要人工处理。
向量时钟能检测冲突,但解决冲突的逻辑要自己写。有没有办法让数据「自动合并,永不冲突」?
下一篇,我们来看 CRDT——一种神奇的数据结构,数学上保证冲突可以自动解决。
下一篇:CRDT:无需协调的合并魔法
本系列: