一致性模型:从强一致到最终一致


前置知识: [../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保证)
幂等性设计使最终一致性系统可以安全重试

小结

⭐ 核心要点:

  1. 一致性是一个谱系:从线性一致性(最强)到最终一致性(最弱),越强越慢
  2. CAP的实际含义:网络分区时必须在”一致”和”可用”间选择,正常情况下几乎可以兼顾
  3. 没有最好的一致性模型:关键看业务需求——金融用强一致性,社交用最终一致性

关联阅读