在单机系统中,数据的一致性、可用性都容易保障。但在分布式系统中,节点之间通过网络通信,而网络是不可靠的——网络分区 (Network Partition) 随时可能发生。Eric Brewer 在 2000 年提出了 CAP 猜想,后被证明为定理:一个分布式系统在 C(一致性)、A(可用性)、P(分区容忍性)三者中最多只能同时满足两个。
一致性 (Consistency): 所有节点在同一时刻看到相同的数据。在分布式数据库中,表现为所有读操作返回最新写入的值。
可用性 (Availability): 每个请求都能收到非错误的响应,但不保证返回的是最新数据。
分区容忍性 (Partition Tolerance): 系统在节点间网络发生分裂(分区)时仍能继续运行。
常见误区: CAP 定理说的是「在分区发生时」必须选择 C 或 A,而不是说系统永远不能同时满足 CA。当网络正常时,系统可以同时具备 C 和 A。分布式系统 P 是必选项,现实只有 CP 和 AP 两类系统。CA 系统仅存在于单机场景(如传统 RDBMS),因为它们不支持网络分区。
# 不同系统对 CAP 的选择
# CP 系统 (如 ZooKeeper, HBase)
# - 优先保证一致性,发生分区时停止写服务
# - 适合需要强一致性的场景 (分布式锁, 元数据管理)
# AP 系统 (如 Cassandra, Dynamo)
# - 优先保证可用性,分区时继续接受写操作
# - 适合容忍最终一致性的场景 (社交网络, 日志收集)
# CA 系统 (如单机 MySQL, 传统 RDBMS)
# - 不支持分区容忍性,所有节点必须在同一网络中
# - 实际上分布式系统中 CA 不可行,因为网络分区总会发生
传统关系数据库强调 ACID 事务,但在分布式系统中,强一致性要求会极大地限制可扩展性和可用性。BASE 理论是 ACID 的对立面,它允许系统在可扩展性和可用性方面有更好的表现,同时弱化了一致性要求。
BA (Basically Available) 基本可用: 系统在大部分时间是可用的,允许出现部分故障,但整体仍能响应请求。例如,在分区发生时,系统只对受影响的部分服务降级,而非完全不可用。
S (Soft State) 软状态: 系统状态随时间变化,不需要始终保持强一致性。数据可以在一段时间内不一致,但这个状态是「软的」,最终会收敛到一致。
E (Eventually Consistent) 最终一致性: 系统不保证数据在写入后立即在所有节点一致,但保证在不进行新的写操作时,所有节点最终会收敛到一致的状态。
# 分布式计数器 (最终一致性)
class DistributedCounter:
def __init__(self):
self.value = 0
self.replicas = [] # 复制节点列表
def increment(self):
# 写操作只在本地节点生效,异步同步到其他节点
self.value += 1
# 异步复制 (最终一致性的关键)
for replica in self.replicas:
# 异步发送更新请求,不等待响应
replica.async_update(self.value)
def read(self):
# 读操作返回当前节点值,可能不是全局最新
return self.value
# 最终所有节点会收敛到相同的值
CAP 和 BASE 让我们知道一致性是有代价的。但在实际系统中,一致性并不是「全有或全无」的。分布式系统提供了多种一致性模型,每种模型在数据一致性和系统性能之间做出不同程度的权衡。从最严格的线性一致性到最宽松的最终一致性,形成了一个光谱。
线性一致性 (Linearizability): 最强的一致性模型。每个操作在某个时间点原子完成,所有后续读操作都能看到该操作的效果。相当于单机系统的行为。
顺序一致性 (Sequential Consistency): 所有操作有一个全局顺序,每个节点的操作顺序与这个全局顺序一致。不同节点看到的操作顺序可能不同,但总体顺序一致。
因果一致性 (Causal Consistency): 只保证有因果关系的一组操作在所有节点上具有相同的顺序。无因果关系的操作可以乱序。
最终一致性 (Eventual Consistency): 最弱的一致性模型。不保证任何读操作看到最新写入,但保证停止写入后,所有节点最终收敛到相同状态。
线性一致性与顺序一致性的区别: 线性一致性要求每个操作的时间点与全局顺序一致,即操作发生的「时间」与「顺序」是统一的。顺序一致性只要求操作有全局顺序,但这个顺序可以不反映真实时间。
# 线性一致性 (Linearizability)
# 特点:每个操作完成后,所有后续读操作都能看到
# 实现:通常需要共识算法 (Paxos/Raft) + 多数派写入
# 代价:高延迟,低并发,可用性较低
# 最终一致性 (Eventual Consistency)
# 特点:写操作快速完成,读操作可能看到旧值,最终收敛
# 实现:异步复制 + 冲突处理机制
# 代价:数据可能暂时不一致,需要应用层处理冲突
# 因果一致性 (Causal Consistency)
# 特点:有因果关系的操作在所有节点上顺序一致
# 实现:向量时钟跟踪因果关系
# 代价:比最终一致性开销大,但比线性一致性低
在分布式系统中,多个节点需要对某个值达成一致,而这个过程中可能出现节点故障、网络分区、消息延迟等各种问题。传统的主从复制在故障时可能丢失数据。共识算法的核心目标是:在存在故障的分布式环境中,让所有正常节点对某个值达成一致。
Paxos 算法 的核心是 Proposer (提议者) 和 Acceptor (接受者) 之间的两个阶段交互:
Raft 算法 将问题分解为三个子问题:
# Raft 节点状态与选举计时器
enum State { FOLLOWER, CANDIDATE, LEADER }
class RaftNode:
def __init__(self):
self.state = State.FOLLOWER
self.current_term = 0
self.voted_for = None
self.log = [] # 日志条目列表
self.commit_index = 0
self.last_applied = 0
def election_timeout(self):
# 选举超时:转换为 Candidate 开始选举
if self.state == State.FOLLOWER:
self.state = State.CANDIDATE
self.current_term += 1
self.voted_for = self.node_id
# 向其他节点发送 RequestVote RPC
def handle_append_entries(self, term, leader_id, entries):
# 如果收到来自更高 term 的 Leader 心跳,转为 Follower
if term >= self.current_term:
self.state = State.FOLLOWER
self.current_term = term
# 检查日志一致性,追加新条目
# 回复成功或失败
在单机系统中,锁可以通过操作系统提供的 mutex 或 semaphore 实现。但在分布式系统中,多个节点需要竞争同一个资源,传统的单机锁无法工作。分布式锁需要解决的关键问题是:在分布式环境中实现互斥访问,同时要应对节点故障、网络分区等挑战。
基于 Redis 的分布式锁 (Redlock): 使用 Redis 的 SET NX 和 EXPIRE 命令实现。但 Redis 锁在故障时可能失效,Redlock 算法通过多个 Redis 节点保证可靠性。
基于 ZooKeeper 的分布式锁: 利用 ZooKeeper 的临时节点 (Ephemeral Node) 和序列节点 (Sequential Node) 特性,创建临时顺序节点,通过监听前一个节点实现锁的排队。
基于 Raft 的分布式锁: 利用 Raft 共识算法保证锁状态的强一致性,如 etcd 和 Consul 提供的分布式锁接口。
# 使用 ZooKeeper 实现分布式锁 (Python 伪代码)
class ZKDistributedLock:
def __init__(self, zk_client, lock_path):
self.zk = zk_client
self.lock_path = lock_path
self.lock_node = None
def acquire(self):
# 创建临时顺序节点,例如 /lock/seq-000000001
self.lock_node = self.zk.create(
self.lock_path + '/seq-',
ephemeral=True,
sequence=True
)
# 获取所有子节点,检查自己是否为最小的
children = self.zk.get_children(self.lock_path)
if self.lock_node == sorted(children)[0]:
return True # 获得锁
else:
# 监听前一个节点,等待锁释放
prev_node = children[sorted(children).index(self.lock_node) - 1]
self.zk.exists(self.lock_path + '/' + prev_node, watch=True)
# 等待前一个节点消失...
def release(self):
if self.lock_node:
self.zk.delete(self.lock_node)
self.lock_node = None
在分布式系统中,一个事务可能涉及多个节点上的数据操作。如何保证这些操作满足 ACID 特性?两阶段提交 (2PC) 是最早提出的分布式事务协议,它通过协调器(Coordinator)和参与者(Participant)之间的两轮交互,确保所有节点要么全部提交,要么全部回滚。
2PC (Two-Phase Commit) 两阶段提交:
3PC (Three-Phase Commit) 三阶段提交: 引入预提交阶段 (Pre-commit),解决 2PC 的阻塞问题。参与者收到预提交指令后,进入准备状态,但尚未实际提交。这样即使协调器故障,参与者也可以超时后自行提交。
TCC (Try-Confirm-Cancel): 应用层补偿事务,将事务分为三个阶段:
# TCC 三阶段:Try → Confirm → Cancel
class TransferTCC:
def try_stage(self, from_account, to_account, amount):
# 预留资金,冻结金额
freeze_balance(from_account, amount)
reserve_balance(to_account, amount)
def confirm_stage(self, from_account, to_account, amount):
# 确认转账,扣除冻结金额,增加目标金额
deduct_frozen(from_account, amount)
add_balance(to_account, amount)
def cancel_stage(self, from_account, to_account, amount):
# 取消操作,释放冻结金额
unfreeze_balance(from_account, amount)
unreserve_balance(to_account, amount)
在分布式系统中,节点之间没有统一的物理时钟,不同节点的时间可能不同步。要判断两个事件的先后顺序,不能依赖物理时间。Lamport 时钟通过逻辑时钟解决了「先发生」关系的部分问题,但对于并发事件无法判断。向量时钟是对 Lamport 时钟的扩展,可以判断并发事件和因果关系。
Lamport 时钟:每个事件有一个逻辑时间戳,满足:
但 Lamport 时钟无法判断两个事件是并发的还是因果相关的。
向量时钟:每个节点维护一个向量 V[1..N],其中 V[i] 表示节点 i 的时钟值。操作规则:
class VectorClock:
def __init__(self, node_id, num_nodes):
self.node_id = node_id
self.clock = [0] * num_nodes
def increment(self):
# 本地事件:增加自身节点的时钟
self.clock[self.node_id] += 1
return self.clock.copy()
def merge(self, other_clock):
# 收到消息:取最大值,然后增加自身节点
for i in range(len(self.clock)):
self.clock[i] = max(self.clock[i], other_clock[i])
self.clock[self.node_id] += 1
def compare(self, other_clock):
# 比较两个向量时钟,判断因果关系
# 返回: -1 (self < other), 1 (self > other), 0 (并发)
less = True
greater = True
for i in range(len(self.clock)):
if self.clock[i] > other_clock[i]:
less = False
if self.clock[i] < other_clock[i]:
greater = False
if less:
return -1
if greater:
return 1
return 0 # 并发