分布式系统是计算机科学中最具挑战性的领域之一。理解其理论基础——CAP、BASE、一致性模型——是设计可靠分布式系统的起点。
1. CAP 定理
1.1 CAP 三选二
2000 年由 Eric Brewer 提出,2002 年被 Gilbert 和 Lynch 形式化证明。
| 属性 | 含义 | 说明 |
|---|---|---|
| C (Consistency) | 一致性 | 所有节点在同一时刻看到相同的数据 |
| A (Availability) | 可用性 | 每个请求都能在有限时间内得到响应(成功或失败) |
| P (Partition Tolerance) | 分区容错性 | 系统在网络分区时仍能继续运行 |
CAP 定理:在出现网络分区时,分布式系统只能在 Consistency 和 Availability 之间二选一。
网络正常时: C + A + P 可同时满足
网络分区时: C + P 或 A + P,不可 C + A
1.2 为什么分区时必须二选一?
假设两个节点 N1、N2,网络分区导致它们无法通信:
- 选 C(一致性):N1 更新数据后,必须同步到 N2 才算成功。由于网络断开,同步失败,N1 必须拒绝写入 → 牺牲可用性。
- 选 A(可用性):N1 允许写入并响应成功,但 N2 看不到最新值 → 牺牲一致性。
1.3 CP vs AP 系统对比
| 系统类型 | 选择 | 代表 | 适用场景 |
|---|---|---|---|
| CP | 一致性优先 | etcd、ZooKeeper、Consul、HBase | 配置中心、锁服务、金融交易 |
| AP | 可用性优先 | Cassandra、Riak、Eureka | 社交feed、日志收集、会话存储 |
| CA | 单节点数据库 | 传统单机 MySQL/PostgreSQL | 非分布式场景 |
注意:CAP 中的 C 指线性一致性(Linearizability),并非数据库事务的 ACID 一致性。
1.4 CAP 的误区
- 不是三选二那么简单:分区是网络故障时的选择,正常网络下 C+A+P 可以共存。
- 不是非黑即白:可以设计为大部分时间 CA,分区时降级为 AP 或 CP。
- 不是系统全局属性:同一系统的不同子系统可以有不同的 CAP 倾向。
2. BASE 理论
BASE 由 eBay 架构师 Dan Pritchett 在 2008 年提出,是对 CAP 的 AP 方向的工程化实践。
| 概念 | 含义 | 对比 ACID |
|---|---|---|
| Basically Available | 基本可用 | 响应可能慢或降级,但不完全拒绝 |
| Soft State | 软状态 | 允许中间状态,数据可不一致 |
| Eventually Consistent | 最终一致性 | 不保证实时一致,但保证最终一致 |
ACID: 强一致性,事务原子性,银行转账
BASE: 最终一致性,电商库存、社交点赞
2.1 最终一致性的实现方式
| 方式 | 机制 | 延迟 |
|---|---|---|
| 读修复 (Read Repair) | 读取时发现不一致,触发修复 | 读取时 |
| 反熵 (Anti-Entropy) | 后台进程定期同步节点数据 | 分钟级 |
| Gossip 协议 | 节点间随机交换状态信息 | 秒级~分钟级 |
2.2 电商场景中的 BASE
// 订单创建:先写入订单,异步扣减库存
@Transactional
public Order createOrder(OrderRequest request) {
// 1. 创建订单(主库,强一致)
Order order = orderRepository.save(new Order(request));
// 2. 发送库存扣减消息(异步,最终一致)
kafkaTemplate.send("inventory-deduct",
new InventoryEvent(order.getId(), request.getSku(), request.getQuantity()));
// 3. 发送延迟消息,15分钟后检查订单状态
kafkaTemplate.send("order-timeout-check",
new DelayedMessage(order.getId(), 15 * 60 * 1000));
return order;
}
// 库存服务消费消息
@KafkaListener(topics = "inventory-deduct")
public void deductInventory(InventoryEvent event) {
inventoryService.decrease(event.getSku(), event.getQuantity());
}
3. PACELC 定理
PACELC 是对 CAP 的扩展,考虑了无分区时延迟 (Latency) 和一致性 (Consistency) 的权衡。
P (Partition) → A (Availability) 或 C (Consistency)
E (Else, 无分区) → L (Latency) 或 C (Consistency)
| 系统 | PACELC 倾向 | 说明 |
|---|---|---|
| DynamoDB | PA/EL | 分区时可用,无分区时低延迟 |
| MongoDB | PC/EC | 默认强一致,可配置为最终一致 |
| Cassandra | PA/EL | 可调一致性级别 (ONE/QUORUM/ALL) |
| HBase | PC/EC | 强一致,基于 HDFS |
4. FLP 不可能定理
Fischer、Lynch、Paterson 在 1985 年证明:在异步网络中,即使只有一个进程故障,也不存在确定性的共识算法。
4.1 系统模型
| 维度 | 分类 |
|---|---|
| 故障模型 | 崩溃停止 (Crash-stop) / 崩溃恢复 / 拜占庭故障 |
| 网络模型 | 同步 / 异步 / 部分同步 |
| 消息传输 | 可靠 / 不可靠 / 有序 / 无序 |
4.2 FLP 的启示
- 同步假设:实际系统通过超时机制(如心跳)将异步转化为同步,从而绕过 FLP。
- 故障检测:Raft/Paxos 使用超时来检测 Leader 故障,本质上是将异步假设放宽为部分同步。
- 非确定性:FLP 针对确定性算法,Las Vegas 或 Monte Carlo 算法(如随机超时)不受限制。
5. 一致性模型
5.1 一致性谱系
强一致性 ←————————————————————————→ 弱一致性
线性一致性 → 顺序一致性 → 因果一致性 → 读己所写 → 单调读 → 最终一致
| 一致性级别 | 定义 | 代表系统 |
|---|---|---|
| Linearizability | 每个操作看起来在调用和返回之间的某个瞬间原子完成 | etcd、ZooKeeper |
| Sequential | 所有进程看到的操作顺序一致,且与程序顺序一致 | 数据库多版本 |
| Causal | 因果相关的操作对所有进程可见顺序一致,无关操作可并发 | COPS、因果数据库 |
| Read-Your-Writes | 进程读到的数据包含自己所有的写 | 会话一致性 |
| Monotonic Reads | 进程不会读到比之前更旧的数据 | 很多缓存系统 |
| Eventual | 无新更新时,所有副本最终一致 | Cassandra、DNS |
5.2 线性一致性(Linearizability)
线性一致性是最强的一致性模型。所有操作看起来按某个全局顺序串行执行,且每个操作在调用到返回之间的某个时间点瞬间完成。
时间线:
P1: ──[W(x=1)]───────────────
P2: ──────────[R(x)?]───────
P3: ─────────────────[R(x)?]─
线性一致性要求:
- P2 的读必须在 W 完成后,返回 1
- P3 的读在 P2 之后,也必须返回 1
测试线性一致性:使用 Jepsen 测试框架。
;; Jepsen 测试示例
(def db (db "etcd"))
(jepsen/run! (assoc tests/noop-test
:name "etcd-linearizable"
:db db
:client (client nil)
:checker (checker/linearizable)
:nemesis (nemesis/partition-random-halves)))
5.3 顺序一致性(Sequential Consistency)
比线性一致性弱:不要求操作按真实时间排序,只要求所有进程看到的操作顺序一致。
时间线:
P1: ──[W(x=1)]──[W(y=1)]───
P2: ────────[R(y)?]──[R(x)?]─
顺序一致性允许:P2 读到 y=1, x=0(W(x) 尚未传播)
但所有进程必须看到同样的操作交错顺序
5.4 因果一致性(Causal Consistency)
只保证因果相关的操作有序。如果事件 A 导致了事件 B(如 A 写 x=1,B 读 x 后写 y=2),则所有进程必须先看到 A 再看到 B。
P1: [W(x=1)]────────────
P2: ────[R(x=1)][W(y=2)]─
P3: ─────────────────[R(y=2)?][R(x)?]
因果一致性:P3 必须 R(x)=1,因为 y=2 因果依赖于 x=1
P3 读到 y=2 的同时,必须也能读到 x=1
6. 分布式时钟
6.1 物理时钟 vs 逻辑时钟
| 类型 | 代表 | 用途 |
|---|---|---|
| 物理时钟 | NTP、TrueTime (Spanner) | 绝对时间戳 |
| 逻辑时钟 | Lamport 时钟、向量时钟 | 事件先后关系 |
6.2 Lamport 时间戳
def lamport_clock(events):
"""Lamport 标量时钟:偏序关系,无法识别并发事件"""
clock = {}
for process, event_type in events:
if process not in clock:
clock[process] = 0
if event_type == 'send':
clock[process] += 1
# 发送消息携带当前时间戳
elif event_type == 'receive':
# 接收方: max(本地时钟, 消息时间戳) + 1
clock[process] = max(clock[process], msg_timestamp) + 1
else: # local event
clock[process] += 1
return clock
6.3 向量时钟 (Vector Clock)
class VectorClock:
def __init__(self, num_processes, process_id):
self.id = process_id
self.clock = [0] * num_processes
def increment(self):
self.clock[self.id] += 1
def update(self, other_clock):
self.clock = [max(a, b) for a, b in zip(self.clock, other_clock)]
self.increment()
def compare(self, other):
"""返回: before, after, concurrent"""
less = all(a <= b for a, b in zip(self.clock, other.clock))
greater = all(a >= b for a, b in zip(self.clock, other.clock))
if less and not greater:
return "before"
if greater and not less:
return "after"
if self.clock == other:
return "equal"
return "concurrent"
向量时钟用于版本向量 (Version Vector) 检测冲突,如 Dynamo、Riak、Cassandra 中的向量时钟解决写冲突。
6.4 TrueTime (Google Spanner)
Spanner 使用 GPS 和原子钟提供外部一致性(External Consistency),即如果事务 T2 在 T1 提交后开始,则 T2 的时间戳一定大于 T1。
TT.now() → [earliest, latest]
等待直到不确定性 interval 足够小:
sleep(max(0, TT.now().latest - TT.now().earliest))
7. 副本一致性协议
7.1 主从复制 (Primary-Backup)
Primary → 同步复制 → Backup1
→ 同步复制 → Backup2
→ 异步复制 → Backup3
| 模式 | 优点 | 缺点 |
|---|---|---|
| 同步复制 | 强一致,RPO=0 | 延迟大,吞吐量低 |
| 异步复制 | 低延迟,高吞吐 | 可能丢失数据 |
| 半同步 | 折中方案 | 配置复杂 |
7.2 仲裁读写 (Quorum)
N = 副本总数
W = 写入成功的最少确认数
R = 读取的最少副本数
强一致性条件: W + R > N
读优化: R = 1, W = N(如 DynamoDB 的写全部)
写优化: W = 1, R = N
折中: W = R = (N+1)/2(如 Cassandra 的 QUORUM)
7.3 Dynamo 风格向量时钟
class DynamoVC:
def __init__(self):
self.versions = {} # {node_id: counter}
def increment(self, node_id):
self.versions[node_id] = self.versions.get(node_id, 0) + 1
def merge(self, other):
for node, count in other.versions.items():
self.versions[node] = max(self.versions.get(node, 0), count)
def is_descendant(self, other):
"""self 是否是 other 的后继版本"""
return all(self.versions.get(n, 0) >= c
for n, c in other.versions.items())
def has_conflict(self, other):
return not (self.is_descendant(other) or other.is_descendant(self))
当 has_conflict 返回 True 时,需要业务层合并冲突(如 LWW 最后写入胜、业务特定的合并逻辑)。
总结
| 理论 | 核心结论 | 工程启示 |
|---|---|---|
| CAP | 分区时必须放弃 C 或 A | 根据业务选择 CP 或 AP |
| BASE | 最终一致是可接受的 | 电商、社交优先 AP |
| PACELC | 无分区时也有 L/C 权衡 | AWS DynamoDB 的设计依据 |
| FLP | 异步网络无确定性共识 | 引入超时打破异步假设 |
| 一致性模型 | 从强到弱有连续谱 | 选择最弱但够用的一致性 |
| 向量时钟 | 检测并发事件 | Dynamo 风格数据存储 |
设计分布式系统的关键决策:
- 确定一致性需求:金融交易需要线性一致性,社交平台可用最终一致性
- 故障模型假设:崩溃停止比拜占庭简单得多
- 网络模型选择:同步假设太强,异步假设太弱,部分同步最实用
- 权衡的艺术:CAP 不是放弃,而是有意识地选择和权衡
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。