本文系统梳理分布式事务的核心难点与解决方案:从 2PC/3PC 的强一致性方案,到 TCC/Saga 的业务层方案,再到本地消息表和事务消息的最终一致性方案。读完能清楚知道各方案的适用场景、核心问题与工程取舍。
相关文章:分布式一致性 · 分布式协调与选举 · 分布式系统设计模式 · 金融级分布式系统实践
目录
| 章节 | 说明 |
|---|---|
| 分布式事务的难点 | 为什么单机事务方案在分布式下失效 |
| 2PC 两阶段提交 | 强一致性方案,协调者/参与者模型 |
| 3PC 三阶段提交 | 2PC 的改进,引入超时机制 |
| TCC | 业务层面的分布式事务,Try-Confirm-Cancel |
| Saga 长事务 | 正向事务 + 补偿事务 |
| 本地消息表 | 最终一致性,基于数据库 |
| 事务消息 | 最终一致性,基于消息队列 |
| 方案对比 | 各方案的核心取舍 |
分布式事务的难点
本地事务(单机)具备 ACID 四个特性:
| 特性 | 含义 |
|---|---|
| A(原子性) | 要么全部执行,要么全部不执行 |
| C(一致性) | 事务前后数据满足完整性约束 |
| I(隔离性) | 并发事务互不干扰 |
| D(持久性) | 提交后永久保存,崩溃后可恢复 |
分布式事务是由多个本地事务组合而成,跨越多台机器甚至多个数据中心。难点在于:
- 网络不可靠:消息可能丢失、延迟、重复
- 节点可能宕机:任意节点在任意时刻都可能故障
- 无全局时钟:无法精确判断事件的先后顺序
- 性能 vs 一致性:保证强一致性需要多轮协商,严重影响吞吐量
典型场景:电商下单 = 订单系统创建订单 + 库存系统减库存。两个操作在不同服务器上,必须同时成功或同时失败(All or Nothing)。
2PC 两阶段提交
基于 XA 协议,通过协调者统一管理所有参与者,实现强一致性。
角色
- 协调者(Coordinator):事务管理器,负责发起和决策
- 参与者(Cohort):本地资源管理器(通常是数据库),执行具体操作
两个阶段
sequenceDiagram
participant C as 协调者
participant P1 as 参与者-1(订单)
participant P2 as 参与者-2(库存)
Note over C,P2: 阶段一:投票(Voting)
C->>P1: CanCommit?
C->>P2: CanCommit?
P1-->>C: Yes(执行操作,记录日志,但不提交)
P2-->>C: No(库存不足)
Note over C,P2: 阶段二:提交(Commit)—— 有 No 则回滚
C->>P1: DoAbort
C->>P2: DoAbort
P1-->>C: HaveCommitted(已回滚)
P2-->>C: HaveCommitted
成功路径:所有参与者返回 Yes → 协调者发送 DoCommit → 所有参与者提交并释放资源。
失败路径:任意参与者返回 No → 协调者发送 DoAbort → 所有参与者回滚。
三个核心问题
| 问题 | 描述 | 影响 |
|---|---|---|
| 同步阻塞 | 参与者持有临界资源锁等待协调者指令,其他请求被阻塞 | 不支持高并发 |
| 单点故障 | 协调者宕机后,参与者永久等待,整个集群停滞 | 可用性低 |
| 数据不一致 | 提交阶段网络异常,部分参与者收到 DoCommit 而部分未收到 | 数据不一致 |
3PC 三阶段提交
对 2PC 的改进,引入超时机制和准备阶段,减少同步阻塞,但无法完全解决数据不一致。
三个阶段
sequenceDiagram
participant C as 协调者
participant P as 参与者
Note over C,P: 阶段一:CanCommit(询问,不加锁)
C->>P: CanCommit?
P-->>C: Yes/No
Note over C,P: 阶段二:PreCommit(预提交,加锁)
C->>P: PreCommit(所有人都 Yes 时)
P-->>C: ACK(执行操作,记录 Undo/Redo 日志)
Note over C,P: 阶段三:DoCommit(正式提交)
C->>P: DoCommit
P-->>C: ACK(提交,释放资源)
2PC vs 3PC 对比
| 维度 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2 | 3 |
| 超时机制 | 仅协调者有 | 协调者和参与者都有 |
| 阻塞问题 | 严重 | 有所改善 |
| 数据不一致 | 存在 | 仍然存在(PreCommit 阶段网络分区) |
| 复杂度 | 低 | 高 |
关键改进:参与者在 PreCommit 后超时未收到 DoCommit,默认提交(而非等待),减少阻塞。但这也可能在网络分区时导致数据不一致。
TCC
TCC(Try-Confirm-Cancel)是业务层面的分布式事务协议,不依赖数据库锁,适合跨服务、跨数据库的复杂业务场景。
三个操作
| 操作 | 含义 | 对应 2PC |
|---|---|---|
| Try | 预留资源,锁定业务数据,确保 Confirm 一定能成功 | 提交请求阶段 |
| Confirm | 正式执行业务操作,使用 Try 预留的资源 | 提交执行阶段 |
| Cancel | 撤销 Try 阶段的预留,释放资源 | 回滚 |
订票系统示例
场景:深圳→上海(深圳航空)+ 上海→北京(上海航空),两段都订成功才算成功。
sequenceDiagram
participant S as 订票系统
participant CA as 深圳航空
participant CB as 上海航空
Note over S,CB: Try 阶段:预留机票
S->>CA: 预留深圳→上海机票
S->>CB: 预留上海→北京机票
CA-->>S: 预留成功
CB-->>S: 预留成功
Note over S,CB: Confirm 阶段:确认订购
S->>CA: 确认订购
S->>CB: 确认订购
CA-->>S: 订购成功
CB-->>S: 订购成功
若 Try 阶段任一失败,则执行 Cancel,撤销已预留的资源。
TCC 的核心要点
- 业务层面,非数据库层面:每个操作对数据库来说是一个本地事务,操作完成后立即释放数据库资源,避免长时间锁定
- 跨服务、跨数据库:可以将多个服务的操作组合为一个原子操作
- Confirm/Cancel 必须幂等:因为这两个操作可能因网络问题重试
- 业务侵入性强:需要为每个业务操作设计 Try/Confirm/Cancel 三个接口
TCC 的陷阱
| 陷阱 | 描述 | 解决方案 |
|---|---|---|
| 空回滚 | Try 未执行,Cancel 被调用 | Cancel 中检查 Try 是否执行,未执行则直接返回成功 |
| 悬挂 | Cancel 先于 Try 执行 | 记录事务状态,Cancel 后拒绝 Try 的执行 |
| 幂等 | Confirm/Cancel 被重复调用 | 通过唯一事务 ID 做幂等控制 |
Saga 长事务
Saga 将一个长事务拆分为一系列本地事务,每个本地事务有对应的补偿事务,适合长时间运行的业务流程。
核心思想
T1 → T2 → T3 → ... → Tn (正向事务链)
C1 ← C2 ← C3 ← ... ← Cn (补偿事务链,逆序执行)
- 正常情况:依次执行 T1, T2, ..., Tn
- 某个 Ti 失败:依次执行 C(i-1), C(i-2), ..., C1 进行补偿
两种协调模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 编排(Choreography) | 每个服务完成后发布事件,下一个服务监听并执行 | 简单流程,服务少 |
| 指挥(Orchestration) | 中央协调者(Saga Orchestrator)指挥每个服务执行 | 复杂流程,易于监控 |
Saga vs TCC
| 维度 | TCC | Saga |
|---|---|---|
| 隔离性 | 较好(Try 阶段预留资源) | 较差(中间状态可见) |
| 业务侵入 | 高(需实现 3 个接口) | 中(需实现补偿事务) |
| 适用场景 | 短事务,需要较强隔离 | 长事务,可接受中间状态 |
| 典型应用 | 支付、库存扣减 | 旅行预订(机票+酒店+租车) |
本地消息表
将消息持久化到本地数据库,利用数据库事务保证消息的可靠投递,实现最终一致性。
流程
sequenceDiagram
participant A as 服务A(订单)
participant DB as 本地数据库
participant MQ as 消息队列
participant B as 服务B(库存)
Note over A,DB: 原子操作:业务操作 + 写消息表
A->>DB: BEGIN TRANSACTION
A->>DB: 创建订单
A->>DB: 插入消息表(状态=待发送)
A->>DB: COMMIT
Note over A,MQ: 定时任务扫描消息表
A->>MQ: 发送消息
MQ-->>A: 发送成功
A->>DB: 更新消息状态=已发送
MQ->>B: 投递消息
B-->>MQ: 消费成功(ACK)
关键点:
- 业务操作和写消息表在同一个本地事务中,保证原子性
- 消息发送失败时,定时任务重试(消费方必须做幂等处理)
- 消息表记录发送状态,已成功的不再重发
优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 实现简单,利用现有数据库能力 |
| 缺点 | 消息表与业务库耦合,数据量大时性能压力大 |
| 适用 | 系统规模不大,已有关系型数据库 |
事务消息
利用消息队列的事务消息特性(如 RocketMQ),将消息发送与本地事务绑定,实现最终一致性。
RocketMQ 事务消息流程
sequenceDiagram
participant P as 生产者(订单服务)
participant MQ as RocketMQ Broker
participant C as 消费者(库存服务)
P->>MQ: 发送半消息(Half Message)
MQ-->>P: 发送成功
P->>P: 执行本地事务(创建订单)
alt 本地事务成功
P->>MQ: Commit(消息可投递)
MQ->>C: 投递消息
C-->>MQ: ACK
else 本地事务失败
P->>MQ: Rollback(消息丢弃)
else 本地事务状态未知(超时)
MQ->>P: 回查事务状态
P-->>MQ: 返回 Commit/Rollback
end
半消息(Half Message):对消费者不可见,只有 Commit 后才能被消费。
RocketMQ vs Kafka 事务对比
| 维度 | RocketMQ 事务消息 | Kafka 事务 |
|---|---|---|
| 设计目标 | 生产者本地事务 + 消息投递的原子性 | Producer 跨多个分区的原子写入 |
| 回查机制 | 有(Broker 主动回查生产者) | 无 |
| 消费者幂等 | 需要业务自行保证 | 需要业务自行保证 |
| 适用场景 | 分布式事务最终一致性 | 流处理 Exactly-Once 语义 |
方案对比
| 方案 | 一致性 | 性能 | 业务侵入 | 适用场景 |
|---|---|---|---|---|
| 2PC | 强一致 | 低(同步阻塞) | 低(数据库支持) | 并发低、强一致要求高 |
| 3PC | 强一致(有缺陷) | 较低 | 低 | 2PC 的改进,实际少用 |
| TCC | 最终一致(业务层强一致) | 高 | 高(需实现 3 接口) | 跨服务短事务,需较强隔离 |
| Saga | 最终一致 | 高 | 中(需实现补偿) | 长事务,可接受中间状态 |
| 本地消息表 | 最终一致 | 中 | 中(需维护消息表) | 规模适中,已有关系型数据库 |
| 事务消息 | 最终一致 | 高 | 低(MQ 原生支持) | 异步场景,消息队列已在使用 |
选型原则:
- 涉及钱、强隔离要求 → TCC 或 2PC
- 长业务流程、可接受中间状态 → Saga
- 异步场景、已用消息队列 → 事务消息
- 简单场景、已有关系型数据库 → 本地消息表
- 避免在高并发系统中使用 2PC(同步阻塞是性能杀手)
参考资料
- 《分布式技术原理与算法解析》— 聂鹏程(极客时间)
- 《分布式协议与算法实战》— 韩健(极客时间)
- Saga Pattern — Chris Richardson
- RocketMQ 事务消息文档
评论 (0)
发表评论