第6章:一致性模型(Consistency Models)
一、导读
1.1 本章学习目标
第6章深入讲解分布式系统中的一致性模型。一致性模型定义了客户端在读写数据时能获得的保证——从最弱的"最终一致性"到最强的"线性一致性",不同的一致性级别对应不同的性能和可用性特征。
本章核心学习目标:
- 理解线性一致性(Linearizability):最强的 consistency 保证
- 掌握顺序一致性(Sequential Consistency):与线性一致性的区别
- 理解因果一致性(Causal Consistency):如何捕捉事件之间的因果关系
- 掌握最终一致性(Eventual Consistency):各种弱一致性模型的统称
- 理解向量时钟(Vector Clock):如何检测并发写入和因果序
1.2 为什么本章如此重要
一致性模型是分布式系统设计的核心决策。选择太强的一致性会导致延迟高、可用性差;选择太弱的一致性会导致业务逻辑复杂(需要处理冲突)。理解各种一致性模型的精确含义,是做出正确设计决策的前提。
1.3 核心问题引导
线性一致性和顺序一致性的关键区别是什么?
为什么向量时钟能检测并发但无法确定因果序?
最终一致性有哪些具体的变体?它们各提供什么保证?
为什么"读己之所写"看起来简单但实现起来并不 trivial?
二、核心概念详解
2.1 线性一致性(Linearizability)
线性一致性是最强的一致性保证:每个操作都像是在某个全局时间点原子地执行的,所有节点在同一时刻看到相同的值。
线性一致性:
Client A: write(X=1) ──── 成功 ────→
↓ 全局时间点 t
Client B: read(X) ── 返回 1 ──→
所有节点在 t 之后都能读到 X=1
操作的效果看起来是在 t 时刻瞬间完成的
实现代价:
- 需要协调多数节点(如 Paxos/Raft)
- 延迟至少为一次网络往返
- 分区时可能不可用2.2 顺序一致性(Sequential Consistency)
顺序一致性保证:所有操作以某种全局顺序执行,每个处理器的操作按程序顺序出现。
顺序一致性 vs 线性一致性:
顺序一致性: 操作有全局顺序,但顺序不一定与实时时间一致
线性一致性: 全局顺序必须与实时时间一致
示例:
Client A: write(X=1) at time 100
Client B: write(X=2) at time 200
顺序一致性允许: 全局顺序为 X=2, X=1 (只要所有节点看到相同顺序)
线性一致性不允许: 因为 X=2 的写入时间晚于 X=1,全局顺序必须是 X=1, X=22.3 因果一致性(Causal Consistency)
因果一致性捕捉事件之间的因果关系:有因果关系的事件必须按因果顺序被所有节点看到,没有因果关系的事件(并发事件)可以以任意顺序被看到。
因果关系:
A: write(X=1)
↓ (因果依赖)
B: read(X=1), write(Y=2)
↓ (因果依赖)
C: read(Y=2)
因果一致性保证: 如果 A 发生在 B 之前,所有节点都先看到 A 的效果再看到 B 的效果
并发事件:
A: write(X=1) ─┐
├→ 这两个事件是并发的,没有因果关系
B: write(Y=2) ─┘
因果一致性允许: 不同节点可能以不同顺序看到 X=1 和 Y=22.4 向量时钟(Vector Clock)
向量时钟是一种检测事件因果关系的数据结构:
向量时钟: [N_A, N_B, N_C] (每个节点一个计数器)
事件发生时: 本地计数器 +1
收到消息时: 每个分量取 max,然后本地计数器 +1
示例:
A: [1,0,0] → 发送消息 → B
B: 收到后 [1,1,0] → 发送消息 → C
C: 收到后 [1,1,1]
比较:
[1,0,0] < [1,1,0] → 因果关系 (A 发生在 B 之前)
[1,0,0] vs [0,1,0] → 并发 (无法比较)2.5 最终一致性的变体
最终一致性是一个宽泛的概念,有多种具体变体:
读己之所写 (Read Your Writes):
- 客户端写入后,后续读取一定能看到自己的写入
- 实现: 客户端携带版本号,读时等待版本 ≥ 写入版本
单调读 (Monotonic Reads):
- 客户端一旦读到某个值,后续读取不会读到更旧的值
- 实现: 将客户端绑定到特定节点,或携带版本号
单调写 (Monotonic Writes):
- 一个客户端的写入不会在后续写入之后被覆盖
- 实现: 将同一客户端的写入路由到同一节点
一致前缀读 (Consistent Prefix Reads):
- 读取结果一定是某个写入序列的前缀
- 实现: 保证写入按顺序传播三、实战案例
3.1 案例一:向量时钟的实现
class VectorClock:
def __init__(self, nodes):
self.clock = {node: 0 for node in nodes}
def increment(self, node):
self.clock[node] += 1
def merge(self, other):
for node in self.clock:
self.clock[node] = max(self.clock[node], other.clock[node])
def compare(self, other):
"""返回: 'happened_before', 'happened_after', 'concurrent', 'equal'"""
le = ge = False
for node in self.clock:
if self.clock[node] < other.clock[node]:
le = True
elif self.clock[node] > other.clock[node]:
ge = True
if not le and not ge:
return 'equal'
elif le and not ge:
return 'happened_before'
elif ge and not le:
return 'happened_after'
else:
return 'concurrent'
def __repr__(self):
return str(self.clock)
# 使用示例
vc_a = VectorClock(['A', 'B', 'C'])
vc_a.increment('A') # A 的事件: [1,0,0]
vc_b = VectorClock(['A', 'B', 'C'])
vc_b.merge(vc_a)
vc_b.increment('B') # B 收到 A 的消息: [1,1,0]
print(vc_a.compare(vc_b)) # 'happened_before'
vc_c = VectorClock(['A', 'B', 'C'])
vc_c.increment('C') # C 独立事件: [0,0,1]
print(vc_a.compare(vc_c)) # 'concurrent'四、本章小结
线性一致性是最强的一致性,保证操作按实时顺序原子执行
因果一致性只保证有因果关系的事件按序,允许并发事件以任意顺序出现
向量时钟通过每个节点维护一个计数器向量来检测因果关系
最终一致性有多种变体,提供不同程度的读写保证
五、思考与练习
思考题
为什么线性一致性在分区时不可用?这与 CAP 定理有什么关系?
向量时钟的"并发"检测结果在业务上意味着什么?如何处理?
"读己之所写"在移动端(网络不稳定)场景下如何实现?
编程练习
实现一个基于向量时钟的 KV 存储,支持因果一致性读写。
模拟网络分区场景,验证线性一致性和最终一致性的行为差异。