Chapter 07

分布式一致性

分布式系统最深的水。CAP 定理、Paxos/Raft 共识算法、
向量时钟与分布式锁,理解一致性问题的本质。

CAP 定理

2000 年,Eric Brewer 提出 CAP 定理:分布式系统在网络分区发生时,只能在一致性(C)和可用性(A)中二选一

CAP 三角: 一致性(Consistency) ● / \ / \ / \ / 无法 \ / 同时达到 \ / \ ●───────────────● 可用性(Availability) 分区容忍(Partition Tolerance) CAP 定理核心:网络分区(P)在分布式系统中不可避免 因此选择是:P + C(牺牲A)或 P + A(牺牲C) CA 系统:传统单机数据库(MySQL 单节点),不考虑网络分区 实际的分布式系统必须具备 P,所以真正的选择是 CP 或 AP CP 系统(优先一致性): ZooKeeper、etcd、HBase 网络分区时,拒绝服务(返回错误)而不返回过时数据 适用:金融系统、配置中心、选举服务 AP 系统(优先可用性): Cassandra、DynamoDB、CouchDB 网络分区时,继续服务但可能读到旧数据 适用:购物车、点赞数、DNS、内容推送
⚠ CAP 误解澄清

CAP 中的 C 是线性一致性(所有节点看到完全一致的最新状态),这是最强的一致性级别。实际系统中有更多选择,不是非此即彼。而且 CAP 只在网络分区发生的那一刻成立——平时 CA 都可以兼顾。

BASE 理论

ACID 是关系数据库的事务保证,BASE 是互联网大规模分布式系统的务实选择:

BA — Basically Available
基本可用
允许系统在故障时降级,牺牲部分功能保核心可用。如:淘宝双11降级评论功能,只保证下单/支付。
S — Soft State
软状态
允许系统中存在中间状态,数据在副本间可以不同步(存在复制延迟),这是允许的。
E — Eventually Consistent
最终一致
系统在没有新写入的情况下,经过一段时间后,所有副本的数据最终会收敛到一致状态。

一致性级别:从强到弱

一致性强度(从强到弱): ┌─────────────────────────────────────────────────────────┐ │ 线性一致性(Linearizability) │ │ 最强。所有操作看起来是瞬时发生的,全局有序 │ │ 代价:高延迟(需要协调所有节点) │ │ 应用:etcd、ZooKeeper、Google Spanner │ ├─────────────────────────────────────────────────────────┤ │ 顺序一致性(Sequential Consistency) │ │ 所有节点看到的操作顺序相同,但不要求实时 │ │ 早于线性一致但晚于因果一致 │ ├─────────────────────────────────────────────────────────┤ │ 因果一致性(Causal Consistency) │ │ 有因果关系的操作保证顺序,无关操作无需排序 │ │ "你看到我的回复,必须先看到我回复的那条帖子" │ ├─────────────────────────────────────────────────────────┤ │ 读自己写(Read-Your-Writes) │ │ 用户总能读到自己刚写的数据(不保证读到别人的最新写) │ ├─────────────────────────────────────────────────────────┤ │ 最终一致性(Eventual Consistency) │ │ 最弱。只保证最终会一致,中间可能不一致 │ │ 应用:DNS、Cassandra、Amazon S3 │ └─────────────────────────────────────────────────────────┘

Paxos 与 Raft:共识算法

共识算法解决的问题:多个节点如何就同一个值达成一致意见,即使部分节点宕机或消息延迟。

Paxos 算法(Leslie Lamport,1989)

Paxos 两阶段流程(简化): 角色:Proposer(提议者)、Acceptor(接受者)、Learner(学习者) 阶段1:Prepare ┌─────────────────────────────────────────┐ │ Proposer 广播 Prepare(n) 给所有 Acceptor │ │ n = 提案编号(全局递增) │ │ │ │ Acceptor 收到 Prepare(n): │ │ 若 n > 之前见过的最大提案号: │ │ 承诺不接受编号 < n 的提案 │ │ 返回之前接受过的值(如果有) │ └─────────────────────────────────────────┘ 阶段2:Accept ┌─────────────────────────────────────────┐ │ Proposer 收到多数 Acceptor 的 Promise │ │ 广播 Accept(n, value) 给多数 Acceptor │ │ │ │ Acceptor 收到 Accept(n, v): │ │ 若未承诺拒绝此提案:接受并回复 │ │ │ │ Proposer 收到多数 Accept → 提案被批准! │ └─────────────────────────────────────────┘ 核心思想:需要「多数派(Quorum)」同意,容忍少数节点失败 5节点集群:允许2个节点失败,仍可运作(3/5 > 50%)

Raft 算法(Diego Ongaro,2014)

Raft 是 Paxos 的「可理解版」,把共识问题分解为三个相对独立的子问题:

Raft 关键角色: Leader(领导者):处理所有写请求,向 Follower 复制日志 Follower(追随者):被动接收 Leader 的日志复制 Candidate(候选人):选举过程中的临时角色 ───────────────────────────────────────────── Leader 选举流程: ① 正常状态:Leader 定期发送心跳给所有 Follower ② Follower 超时(未收到心跳): 转变为 Candidate,Term +1 给自己投票,广播 RequestVote 给其他节点 ③ 收到多数票 → 成为新 Leader 开始向所有 Follower 发送心跳,宣告主权 ───────────────────────────────────────────── 日志复制流程: Client ──▶ Leader Leader: 1. 将命令追加到本地日志 2. 并行发送 AppendEntries 给所有 Follower 3. 收到多数 Follower 确认 → 提交(Commit) 4. 返回客户端成功 5. 通知所有 Follower 提交 即使 Leader 在步骤4后宕机: 多数 Follower 已有此日志 新 Leader 会重新复制给剩余节点 数据不丢失!

向量时钟(Vector Clock)

分布式系统中,节点各自有本地时钟,无法用时间戳判断事件的先后顺序。向量时钟通过逻辑计数器追踪因果关系。

向量时钟示例(3个节点:A、B、C): 每个节点维护一个向量 [A_count, B_count, C_count] A 初始:[1,0,0] B 初始:[0,1,0] C 初始:[0,0,1] ① A 发送消息给 B,携带时钟 [1,0,0]: B 收到后:取 max([1,0,0], [0,1,0]) = [1,1,0],B计数+1 = [1,2,0] ② B 发送消息给 C,携带时钟 [1,2,0]: C 收到后:取 max([1,2,0], [0,0,1]) = [1,2,1],C计数+1 = [1,2,2] 比较两个事件 e1=[1,2,0] 和 e2=[1,2,2]: e1 中每个值 ≤ e2 的对应值 → e1 happened-before e2(因果关系) 比较 e1=[2,0,0] 和 e2=[0,2,0]: 既不是 e1≤e2,也不是 e2≤e1 → 并发事件(concurrent),无法判断顺序 应用:Amazon DynamoDB 使用向量时钟检测并发冲突

分布式锁

多个服务实例需要互斥访问共享资源时,本地锁无效,需要分布式锁。

基于 Redis 的分布式锁(Redlock)

Redis 简单分布式锁(单节点): SET lock_key unique_token NX EX 30 ↑ ↑ ↑ ↑ ↑ 命令 Key 唯一值(防误删) NX=不存在才设置 EX=超时秒数 获取锁: result = redis.SET("resource:lock", uuid, NX=True, EX=30) if result == "OK": # 获取成功 try: do_work() finally: # 原子性释放:只删除自己的锁 redis.eval(""" if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) end """, keys=["resource:lock"], args=[uuid]) ───────────────────────────────────────────── Redlock 算法(多节点,更安全): 5个独立的 Redis 节点 向所有5个节点尝试获取锁 收到3个以上成功(多数派)→ 加锁成功 整个过程耗时 < 锁有效期 / 2 释放时向所有节点发送解锁命令

基于 ZooKeeper 的分布式锁

ZooKeeper 分布式锁(更强保证): 原理: 1. 所有竞争者在 /locks/resource/ 下创建临时有序节点 如:/locks/resource/lock-0000001 /locks/resource/lock-0000002 /locks/resource/lock-0000003 2. 获取 /locks/resource/ 下所有子节点,排序 3. 如果自己是序号最小的节点 → 获得锁! 4. 否则 → Watch 比自己小的前一个节点 5. 当前一个节点消失时 → 重新检查是否轮到自己 优点:公平锁(先来先得),不会有惊群效应 ZK 节点挂掉(临时节点),锁自动释放(防死锁) 缺点:ZK 操作比 Redis 慢(需要 Paxos 共识)
▶ 面试要点