
数据库(七):分布式事务——两阶段提交、Saga 模式,以及为何共识如此困难
分布式数据库如何跨机器协调事务——两阶段提交(2PC)、Raft 共识、Saga 模式,以及 outbox 和 CDC 等实用模式。
第 3 篇文章中关于事务的所有内容,均基于单数据库服务器假设——一台机器、一份事务日志、一个锁管理器。一旦数据分布到多台机器上——例如实施分片(sharding)、采用微服务架构(各服务独享数据库)或启用强一致性复制——便直面分布式系统最棘手的问题:多台机器如何就某个值达成一致?
分布式事务问题#
考虑一个电商系统,订单服务和库存服务彼此分离,各自拥有独立数据库:
| |
若订单插入成功而库存更新失败(网络故障、约束冲突或进程崩溃),商品便从未被预留;若缺乏协调机制,数据不一致将不可避免。
在单数据库中,用 BEGIN ... COMMIT 将二者包裹即可解决。但在两个数据库之间,这是不可能的——它们拥有各自独立的事务日志、独立的崩溃恢复机制、独立的时钟。
两阶段提交(2PC)#
分布式事务的经典教科书解法。由一个协调者节点(coordinator)主导协议,多个参与者节点(participants)配合执行。

协议流程#
| |
若任一参与者在 Phase 1 投“否”票,则协调者向所有参与者发送 ROLLBACK。
| |
协调者失效问题(The Coordinator Failure Problem)#
2PC 的关键缺陷是:协调者若在发送 PREPARE 后、 COMMIT 或 ROLLBACK 前崩溃,参与者即陷入僵持——它们已投“是”并持有锁,却无法得知最终决策。
| |
这称为 阻塞问题(blocking problem):参与者须等待协调者恢复并揭晓最终决策——可能无限期。实践中这意味着:
- 锁被无限期持有,阻塞其他事务;
- 可能需要人工干预;
- 该协议不具备容错能力。
2PC 在实践中的应用#
尽管存在局限性, 2PC 仍在实际系统中被使用。
| |
| |
三阶段提交(3PC)#

3PC 在 PREPARE 和 COMMIT 之间增加了一个 PRE-COMMIT 阶段,使参与者可在协调者失效时自主恢复:
| |
理论上, 3PC 是非阻塞的;但实践中却极少使用,原因在于:
- 网络分区仍可能导致不一致(某参与者可能未收到 PRE-COMMIT);
- 额外的一次往返增加了延迟;
- Raft/Paxos 等共识协议更稳健地解决了该问题。
共识算法(Consensus Algorithms)#
共识问题是:即使部分节点发生故障,多个节点仍需就某个值达成一致,这是强一致性分布式数据库的基石。
Paxos (概念性)#
Paxos (Leslie Lamport, 1989)是首个经严格证明正确的共识算法。它定义三类角色:
- 提议者(Proposers):提出值;
- 接受者(Acceptors):投票批准提议;
- 学习者(Learners):获知最终选定的值。
Single-Decree Paxos 的简化视图如下:
| |
Paxos 在理论上正确,但以难以实现而闻名。正如 Lamport 所言,社区花了数年才真正理解他的论文。这种复杂性催生了 Raft。
Raft:可理解的共识协议#
Raft (Diego Ongaro & John Ousterhout, 2014)旨在提供与 Paxos 等价、但更易理解的共识方案。它将共识分解为三个子问题:

- 领导者选举(Leader election)
- 日志复制(Log replication)
- 安全性(Safety)
领导者选举#
各节点初始均为 追随者(Follower),若在随机超时(如 150–300 ms)内未收到领导者心跳,即转为 候选人(Candidate) 并发起选举。
| |
| |
日志复制#
领导者当选后,接收客户端请求并追加至本地日志,再将日志条目复制给追随者:
| |
| |
Raft 的实际应用#
| 系统 | Raft 使用场景 |
|---|---|
| etcd | 键值存储(Kubernetes 后端存储) |
| CockroachDB | 每个 range (分区)使用独立的 Raft 组 |
| TiKV | TiDB 的存储层 |
| Consul | 服务发现与配置管理 |
| RethinkDB | 集群协调 |
Saga 模式#
当 2PC 成本过高或不切实际——这在微服务架构中几乎总是如此——Saga 模式便成为替代方案。它不依赖跨服务的分布式事务,而是将业务流程拆解为一系列本地事务,每项均配有一个 补偿事务(compensating transaction),用于在后续步骤失败时回退影响。

编排(Orchestration) vs. 协作(Choreography)#
协作式(Choreography):各服务发布事件,下游服务监听并响应。
| |
编排式(Orchestration):由中心化协调器(orchestrator)依次指挥各服务。
| |
补偿事务(Compensating Transactions)#
每个正向操作都需配套一个补偿操作。
| 步骤 | 正向操作 | 补偿操作 |
|---|---|---|
| 1 | 创建订单(状态: pending) | 取消订单(状态: cancelled) |
| 2 | 预留库存(stock - 1) | 释放库存(stock + 1) |
| 3 | 扣款支付 | 退款 |
| 4 | 发货 | 取消发货 |
| |
时钟同步与全局排序#
分布式事务需要对操作顺序达成一致。但分布式系统没有共享时钟——每个节点各自维护时钟,而且会漂移。本节解释生产系统如何解决排序问题。
时钟问题#
| |
物理时钟不足以确定顺序。三种方案解决这个问题:
Lamport 时钟与向量时钟#
| |
向量时钟能实现因果一致性,但不提供全序。对于可串行化分布式事务,需要更强的机制。
混合逻辑时钟(HLC)#
CockroachDB 和 YugabyteDB 使用 HLC——物理时间与逻辑计数器的组合:
| |
| |
CockroachDB 的时钟偏移默认值为 500ms。保持 NTP 紧凑(< 250ms)可减少事务重启。
Google Spanner 与 TrueTime#
Spanner 用硬件解决时钟问题:每个数据中心部署 GPS 接收器和原子钟,提供有界不确定性的时间 API。
| |
Spanner 的提交协议使用 TrueTime 分配全局有意义的时间戳:
| |
| |
| 系统 | 时钟机制 | 不确定性 | 排序保证 |
|---|---|---|---|
| Spanner | GPS + 原子钟(TrueTime) | 1-7ms | 外部一致性(线性一致) |
| CockroachDB | NTP + HLC | ~250-500ms | 可串行化(非严格线性一致) |
| YugabyteDB | NTP + HLC | 可配置 | 可串行化 |
| TiDB | TSO(集中式时间戳预言机) | 0(单点) | 线性一致(但 TSO 是瓶颈) |
实际影响#
| |
生产级 Saga 实现#
前面展示的 Saga 伪代码捕获了概念,但生产环境的 Saga 需要:持久化状态、幂等步骤、带退避的重试、超时处理和可观测性。以下是生产级模式。
状态机设计#
| |
Saga 状态数据库 Schema#
| |
编排器实现#
| |
僵死 Saga 恢复#
如果编排器在执行中途崩溃,Saga 会卡住。后台清扫器负责恢复:
| |
Saga 步骤的幂等性#
每个 Saga 步骤必须幂等——安全重试而无副作用:
| |
Saga 可观测性#
| |
线性一致性(Linearizability) vs. 可串行化(Serializability)#
这两个术语常被混淆,但描述的是完全不同的保证。
可串行化(Serializability)(来自事务):并发执行多个事务的结果,等价于这些事务以某种串行顺序执行的结果。它关注的是事务与数据库层面的正确性。
线性一致性(Linearizability)(来自分布式系统):每个操作看起来都在其调用与完成之间的某个瞬时点原子生效;一旦写入被确认,所有后续读取都必须看到该值。它关注的是单个操作与实时顺序。
| |
| 属性 | 可串行化(Serializability) | 线性一致性(Linearizability) |
|---|---|---|
| 范围 | 多操作事务 | 单个操作 |
| 排序要求 | 存在某种串行顺序(任意顺序均可) | 实时顺序(real-time order) |
| 关键场景 | 数据库 | 分布式键值存储、分布式锁 |
| 示例系统 | 任何 SERIALIZABLE 隔离级别的数据库 | ZooKeeper、 etcd、 Spanner |
严格可串行化(Strict serializability) = 可串行化 + 线性一致性。这是最强的一致性保证,也是 Google Spanner 提供的保证。
最终一致性(Eventual Consistency)#

在线性一致性的另一端是最终一致性:若不再有新的写入,所有副本将最终收敛到相同值。

“最终”没有明确定义——收敛时间可能短至毫秒,也可能长达数分钟。实践中:
| |
默认采用最终一致性的系统包括:
- DynamoDB (除非显式请求强一致性读取);
- Cassandra (一致性级别设为 ONE);
- DNS;
- CDN 缓存。
它适用于以下场景:
- 陈旧数据可容忍(社交媒体动态流、商品推荐);
- 系统能检测并解决冲突(购物车、 CRDT);
- 性能比一致性更重要(分析、日志)。
真实世界常用模式#
Outbox 模式#
如何原子性地更新数据库 并 向消息中间件(如 Kafka)发布消息?你无法在数据库与 Kafka 之间使用分布式事务。
Outbox 模式:将消息写入数据库内的一个 “outbox” 表(与业务更新在同一事务中),再由独立进程读取该表并向消息中间件发布。
| |
一个独立的发布进程(或 Debezium + CDC)持续读取 outbox 表并向 Kafka 发布事件:
| |
变更数据捕获(CDC)#
不主动写 outbox 表,而是直接从数据库的事务日志(WAL / Binlog)中捕获变更:
| |
| |
CDC 相较于 Outbox 模式的优点:
- 无需修改应用代码或数据库 schema;
- 捕获所有变更,而非仅靠人工“记得”写 outbox 的那些;
- 更低延迟(直接读 WAL);
- 规避双写风险(dual-write risk)。
何时应避免分布式事务?#
最好的分布式事务,就是你根本不需要的那个。 替代策略包括:
将需事务协同的数据保留在同一节点:设计分片键(partition key),使关联数据共置(co-locate)。
接受最终一致性:许多业务流程天然异步(邮件、通知、分析)。
使用幂等操作(Idempotent operations):设计操作使其重试安全。
| |
面向补偿设计(Design for compensation):不追求预防不一致,而是在事后检测并修复。银行正是这样做的——每日运行对账(reconciliation)流程。
使用单数据库:若你的微服务共享同一数据库(虽属“异端”,但务实),则可直接使用常规事务。
下一步#
我们已覆盖理论:数据如何存储、查询、复制、分片与事务处理。但仅有理论远远不够。在最后一篇中,我们将走向实战:生产环境中的数据库——迁移、监控、连接池、备份、容量规划,以及来自真实线上事故的“战争故事”。