强一致性很美好,但代价是延迟和可用性。亚马逊的 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,可能读到旧值。

常见配置

配置WR特点
强一致N1写慢,读快,写入要等所有副本
强一致1N写快,读慢,读取要查所有副本
平衡22(N=3) 写读都要多数派
最终一致11最快,但可能读到旧值

亚马逊的选择:对购物车,用 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: 美国的朋友看到了

这种短暂不一致能接受吗? 能。社交场景不需要全球同时看到。

最终一致性的变种

根据「最终」的时间窗口和保证强度,有不同的一致性级别:

级别含义延迟
因果一致性有因果关系的操作保序毫秒级
读己之写自己写的数据自己立刻能读到毫秒级
会话一致性同一会话内保证读己之写毫秒级
单调读不会读到比之前更旧的数据毫秒级
最终一致只保证「最终」一致秒~分钟级

大多数应用需要的是「读己之写」:我刚发的帖子,我自己必须能立刻看到。

什么时候用最终一致性?

适合不适合
社交动态银行转账
购物车库存扣减
用户浏览记录订单创建
点赞数支付
缓存分布式锁
日志收集配置中心

判断标准

  1. 短暂不一致会导致金钱损失吗?
  2. 短暂不一致会导致用户体验严重受损吗?
  3. 数据冲突时,有简单的合并策略吗?

如果答案是「不会」「不会」「有」,就可以考虑最终一致性。

常见问题

Q:最终一致性的「最终」是多久?

A:没有保证,取决于系统设计。

  • 正常情况:毫秒到秒级
  • 网络拥塞:秒到分钟级
  • 网络分区:可能是小时级,直到分区恢复

你能接受的延迟,决定了系统的设计。

Q:最终一致性会丢数据吗?

A:不一定,取决于配置。

  • W=1 且节点挂了:如果数据还没同步出去,可能丢失
  • W=2 且配合持久化:至少两个节点有数据,丢失概率很低

Dynamo 的选择是:宁可数据冲突,也不丢数据。冲突可以合并,丢了就没了。

Q:能不能混合使用?

A:可以,这是最佳实践。

电商系统:
  库存服务:强一致性(CP)
  购物车服务:最终一致性(AP)
  商品评论:最终一致性(AP)
  订单服务:强一致性(CP)

不同业务不同对待。

总结

最终一致性的本质

用一致性换可用性,用「延迟一致」换「永远可用」。

Dynamo 的贡献

贡献说明
Quorum 机制灵活控制一致性和可用性的平衡
向量时钟追踪并发,识别冲突
读修复 + 反熵最终达成一致的机制
去中心化没有单点故障

核心取舍

选择得到失去
强一致数据永远正确可用性、延迟
最终一致永远可用、低延迟短暂不一致

最终一致性解决了「可用性」问题,但有一个麻烦:冲突需要人工处理

向量时钟能检测冲突,但解决冲突的逻辑要自己写。有没有办法让数据「自动合并,永不冲突」?

下一篇,我们来看 CRDT——一种神奇的数据结构,数学上保证冲突可以自动解决。


上一篇:Paxos 与 Raft:让多数人达成共识

下一篇:CRDT:无需协调的合并魔法

本系列:

  1. 单机到分布式:一致性为何变难
  2. 2PC 与 CAP:理想的破灭
  3. Paxos 与 Raft:让多数人达成共识
  4. 最终一致性:不强求,但终会一致(本篇)
  5. CRDT:无需协调的合并魔法
  6. 现代方案:从 Spanner 到 TiDB
  7. 实战篇:方案选型与落地