分布式事务:2PC / TCC / 本地消息表 / Saga

单机事务靠数据库 ACID 就够了。拆成微服务后,一个”下单”操作可能横跨订单库、库存库、积分库——数据库管不了跨库的原子性了。分布式事务就是解决”这几个库要么全成功、要么全回滚”的问题。但代价是性能和复杂度,所以现实中的选择题是:你要强一致还是要高可用?


一、为什么需要分布式事务?


sequenceDiagram

    participant User as 用户

    participant Order as 订单服务

    participant Stock as 库存服务

    participant Points as 积分服务

    User->>Order: 下单

    Order->>Order: 订单库: 创建订单 ✅

    Order->>Stock: 扣减库存

    Stock->>Stock: 库存库: 扣减成功 ✅

    Order->>Points: 增加积分

    Points--xPoints: 积分库: 网络超时 ❌

    Note over Order,Points: 积分没加上,但订单和库存都成功了<br/>数据不一致!

单机事务用数据库的 ACID(Atomicity, Consistency, Isolation, Durability)就能保证原子性。但微服务拆分后,每个服务有独立的数据库,跨库事务超出了单个数据库的能力范围——这就是分布式事务存在的原因。


二、四种主流方案对比


graph TD

    subgraph "强一致性方案"

        XA[ 2PC / XA<br/>数据库层面<br/>同步阻塞 ]

        ThreePC[ 3PC<br/>2PC改进版<br/>引入超时 ]

    end

    subgraph "最终一致性方案"

        TCC[ TCC<br/>业务层补偿<br/>Try/Confirm/Cancel ]

        MSG[ 本地消息表<br/>本地事务+MQ<br/>异步重试 ]

        Saga[ Saga<br/>正向+反向补偿链<br/>长事务友好 ]

    end

    XA -->|性能太差| TCC

    TCC -->|开发成本高| MSG

    MSG -->|简单实用| Saga

    style XA fill:#e63946 color:#fff

    style TCC fill:#f4a261 color:#000

    style MSG fill:#2a9d8f color:#fff

    style Saga fill:#264653 color:#fff

方案一致性性能业务侵入适用场景
2PC/XA强一致低(同步阻塞)低(数据库支持)金融核心转账
TCC最终一致高(三个方法)高并发支付/库存
本地消息表最终一致异步通知(订单→积分)
Saga最终一致中(补偿事务)长事务(旅游预订)

国内互联网实践:Seata AT + 本地消息表覆盖 80% 场景,TCC 覆盖 15% 高并发场景,其余方案占 5%。


三、2PC(两阶段提交)

3.1 原理


sequenceDiagram

    participant C as 协调者 Coordinator

    participant P1 as 参与者1 (订单库)

    participant P2 as 参与者2 (库存库)

    Note over C,P2: === Phase 1: 准备阶段 (Prepare) ===

    C->>P1: 准备事务

    P1->>P1: 执行SQL,写undo/redo log<br/>锁定资源

    P1-->>C: 就绪(YES)

    C->>P2: 准备事务

    P2->>P2: 执行SQL,写undo/redo log<br/>锁定资源

    P2-->>C: 就绪(YES)

    Note over C,P2: === Phase 2: 提交阶段 (Commit) ===

    C->>P1: 提交(COMMIT)

    P1->>P1: 正式提交,释放锁

    P1-->>C: 确认

    C->>P2: 提交(COMMIT)

    P2->>P2: 正式提交,释放锁

    P2-->>C: 确认

准备阶段:协调者问所有参与者”能不能提交” → 参与者执行事务但不提交,写 undo/redo log 并锁定资源 → 回复 YES/NO。

提交阶段:所有参与者都回复 YES → 协调者发 COMMIT → 参与者正式提交并释放锁;任一回复 NO → 协调者发 ROLLBACK。

3.2 优缺点

优点缺点
实现简单,数据库原生支持同步阻塞:参与者锁定资源等待协调者,高并发下吞吐骤降
强一致性保证单点故障:协调者宕机 → 参与者无限阻塞
数据不一致风险:Phase 2 部分提交成功部分失败 → 数据不一致

四、TCC(Try-Confirm-Cancel)

4.1 原理

TCC 把一个分布式事务拆成三个业务层面的方法:


sequenceDiagram

    participant App as 订单服务

    participant Stock as 库存服务

    participant Points as 积分服务

    Note over App,Points: === Try: 预留资源 ===

    App->>Stock: Try: 冻结库存 (可用库存-1,冻结库存+1)

    Stock-->>App: OK

    App->>Points: Try: 预增积分 (积分status=pending)

    Points-->>App: OK

    Note over App,Points: === Confirm: 确认执行 ===

    App->>Stock: Confirm: 扣减冻结库存 (冻结库存-1)

    Stock-->>App: OK

    App->>Points: Confirm: 确认积分 (status=confirmed)

    Points-->>App: OK

Try:预留资源(冻结库存、预增积分)——但不真正完成业务。

Confirm:确认执行,把预留的资源真正扣减/生效。

Cancel:取消预留,释放冻结的资源。

4.2 优缺点

优点缺点
性能高(锁粒度是业务资源,非数据库行锁)开发成本极高:每个操作要实现 Try/Confirm/Cancel 三个方法
业务可控,回滚逻辑明确幂等性要求:Confirm/Cancel 必须幂等(重复调用结果一样)
适用于资金等高一致性场景空回滚:Try 没执行就收到 Cancel(网络超时),需要正确处理

4.3 关键设计原则

  • 幂等性:Confirm 和 Cancel 可能被重复调用,必须保证幂等。

  • 空回滚处理:Try 超时未执行就收到 Cancel,Cancel 要识别并返回成功。

  • 悬挂防护:Cancel 先于 Try 到达(网络延迟),后续的 Try 要拒绝。


五、本地消息表

5.1 原理

核心思想:把”写业务数据”和”发消息”放进同一个本地事务


sequenceDiagram

    participant Order as 订单服务

    participant DB as 订单DB+消息表

    participant MQ as 消息队列

    participant Points as 积分服务

    Order->>DB: 本地事务: ①写订单 ②写消息表(status=PENDING)

    DB-->>Order: 事务提交成功

    loop 定时任务轮询

        Order->>DB: 查询PENDING消息

        Order->>MQ: 发送消息

        MQ-->>Order: 发送成功

        Order->>DB: 更新消息状态=SENT

    end

    MQ->>Points: 消费消息: 增加积分

    Points->>Points: 执行业务

    Points-->>MQ: ACK

5.2 流程详解

  1. 本地事务:订单服务在同一个数据库事务中,同时写入订单记录和消息记录(status=PENDING)。

  2. 定时轮询:独立的定时任务扫描消息表,将 PENDING 消息投递到 MQ。

  3. 下游消费:积分服务消费消息并执行业务,完成后 ACK。

  4. 幂等消费:下游必须幂等处理(用消息 ID 去重),防止重复消费。

  5. 重试兜底:消费失败 → MQ 重试 → 重试耗尽 → 进入死信队列 → 人工处理。

5.3 优缺点

优点缺点
实现简单,基于现有数据库和 MQ与业务耦合(每个服务要维护消息表)
业务侵入性低依赖定时任务轮询,实时性有延迟
可靠性高(本地事务保证原子性)消息表需要定期清理

六、Saga

6.1 原理

把一个长事务拆成一系列短事务,每个短事务有对应的补偿事务。某一步失败,按反向顺序执行补偿。


sequenceDiagram

    participant Saga as Saga 编排器

    participant A as 订单服务

    participant B as 库存服务

    participant C as 积分服务

    Note over Saga,C: === 正向执行 ===

    Saga->>A: T1: 创建订单

    A-->>Saga: 成功

    Saga->>B: T2: 扣减库存

    B-->>Saga: 成功

    Saga->>C: T3: 增加积分

    C--xxC: 失败! ❌

    Note over Saga,C: === 反向补偿 ===

    Saga->>B: C2: 恢复库存 (T2的补偿)

    B-->>Saga: 成功

    Saga->>A: C1: 取消订单 (T1的补偿)

    A-->>Saga: 成功

6.2 两种编排模式


graph TD

    subgraph "编排式(Orchestration)"

        O[ 中心编排器 ] --> A[ T1 ]

        O --> B[ T2 ]

        O --> C[ T3 ]

        O --> CR1[ C2 补偿T2 ]

        O --> CR2[ C1 补偿T1 ]

    end

    subgraph "协同式(Choreography)"

        A2[ T1完成→触发T2 ] --> B2[ T2完成→触发T3 ]

        B2 --> C2[ T3失败→触发C2 ]

        C2 --> A22[ C2完成→触发C1 ]

    end

    style O fill:#264653 color:#fff

    style A2 fill:#2a9d8f color:#fff

模式特点优缺点
编排式中心编排器控制流程流程清晰,但编排器是单点
协同式服务间事件驱动无中心,但流程分散难追踪

6.3 优缺点

优点缺点
长事务友好(每步独立提交,不长时间锁资源)只能保证最终一致性
补偿事务实现相对简单补偿逻辑可能很复杂(如已发邮件无法撤回)
业务侵入性中等(只需实现正向+补偿)缺乏隔离性(中间状态对外可见)

七、方案选型决策树


graph TD

    Start[ 分布式事务方案选型 ] --> Q1{需要强一致性?}

    Q1 -->|是| Q1a{高并发?}

    Q1a -->|否| XA[ 2PC / XA<br/>金融核心转账 ]

    Q1a -->|是| TCC[ TCC<br/>秒杀库存扣减 ]

    Q1 -->|否| Q2{长事务/多步骤?}

    Q2 -->|是| Saga[ Saga<br/>旅游预订流程 ]

    Q2 -->|否| Q3{有MQ基础设施?}

    Q3 -->|是| MsgTable[ 本地消息表<br/>订单→积分通知 ]

    Q3 -->|否| Q4{支付回调类?}

    Q4 -->|是| Effort[ 最大努力通知<br/>+对账兜底 ]

    Q4 -->|否| AT[ Seata AT<br/>零侵入首选 ]

    style XA fill:#e63946 color:#fff

    style TCC fill:#f4a261 color:#000

    style MsgTable fill:#2a9d8f color:#fff

    style Saga fill:#264653 color:#fff

    style AT fill:#52b788 color:#fff


八、延伸追问

Q1:2PC 的阻塞问题怎么产生的?

准备阶段参与者锁定资源后,必须等待协调者的最终指令。如果协调者宕机 → 参与者无限阻塞 → 资源被长时间占用 → 系统吞吐骤降。

Q2:TCC 的 Try 失败了怎么办?

Try 失败 → 不执行 Confirm,直接执行已成功部分的 Cancel。例如:订单 Try 成功、库存 Try 失败 → 订单 Cancel(取消订单)。

Q3:本地消息表怎么保证消息不丢?

本地事务同时写业务数据和消息记录,原子性保证”要么全写、要么全不写”。定时任务轮询发送,失败重试。配合幂等消费 + 死信队列 + 对账兜底,三重保障。

Q4:Saga 的隔离性问题怎么解决?

Saga 的中间状态对外可见(T2 扣了库存但 T3 还没执行)。解决方案:① 业务层面加版本号/状态标记;② 读写锁(在 Try 阶段预留资源);③ 语义锁(业务状态标记为”处理中”)。

Q5:Seata 框架支持哪些模式?

AT(自动补偿,零侵入,最常用)、TCC(高性能)、Saga(长事务)、XA(强一致)。国内 80% 场景用 Seata AT + 本地消息表就够了。


一句话总结

分布式事务的本质是在一致性和性能之间做权衡。2PC/XA 强一致但同步阻塞;TCC 性能好但开发成本高;本地消息表简单实用是大多数场景的首选;Saga 适合长事务。学习重点:2PC 两阶段流程、TCC 三个方法设计原则、本地消息表原子性保证、Saga 补偿机制。国内实战推荐:Seata AT + 本地消息表覆盖 80% 场景。


速记卡(面试闪卡)

Q1:一句话讲清「分布式事务:2PC / TCC / 本地消息表 / Saga」到底是什么?

A:解决微服务跨库”要么全成要么全回滚”的一致性问题。

Q2:一、为什么需要分布式事务? —— 怎么理解?

A:像下单横跨订单/库存/积分三个库,积分超时但前两步已成功就数据不一致。单机靠 ACID,跨库得靠分布式事务兜原子性。

Q3:二、四种主流方案对比 —— 怎么理解?

A:像四把锁选一把:2PC/XA 强一致但同步阻塞;TCC 高性能但开发贵;本地消息表简单实用;Saga 长事务友好。国内 Seata AT+消息表覆盖 80%。

Q4:三、2PC(两阶段提交) —— 怎么理解?

A:像开会表决:准备阶段协调者问”能提交吗”,参与者锁资源回 YES/NO;全 YES 才发 COMMIT,否则 ROLLBACK。缺点是同步阻塞、协调者单点。

Q5:四、TCC(Try-Confirm-Cancel) —— 怎么理解?

A:像订酒店先预留再确认:Try 冻结资源、Confirm 真正扣减、Cancel 释放。业务层三方法,要幂等、防空回滚、防悬挂。

Q6:核心速记主线有哪些?

  • 2PC/XA:准备+提交两阶段,强一致但同步阻塞

  • TCC:Try 预留/Confirm 确认/Cancel 取消,要幂等

  • 本地消息表:本地事务写业务+消息,轮询投 MQ 保原子

  • Saga:正向短事务+反向补偿,适合长事务

口诀

A:跨库要原子

2PC表决迟

TCC三步走

消息Saga补

相关链接