分布式事务详解


目录

  1. 分布式事务背景
  2. 2PC(两阶段提交)
  3. 3PC(三阶段提交)
  4. TCC(Try-Confirm-Cancel)
  5. Saga 模式
  6. AT 模式
  7. 可靠消息模式
  8. 阿里 Seata 分布式事务框架
  9. 总结对比

一、分布式事务背景

1.1 从单体到分布式

在传统的单体架构中,应用的所有模块运行在同一个进程中,共享同一个数据库实例。数据库事务(ACID)天然保证了一致性:

┌────────────────────────────────┐
│         单体应用               │
│  ┌──────┐ ┌──────┐ ┌──────┐  │
│  │订单模块│ │库存模块│ │支付模块│  │
│  └──┬───┘ └──┬───┘ └──┬───┘  │
│     └────────┼────────┘       │
│         ┌────▼────┐           │
│         │ 单一数据库 │           │
│         └─────────┘           │
└────────────────────────────────┘

随着业务规模增长,单体架构暴露出大量问题:代码耦合严重、扩展困难、部署周期长、故障隔离性差。于是微服务/分布式架构应运而生:

┌──────────┐  ┌──────────┐  ┌──────────┐
│ 订单服务  │  │ 库存服务  │  │ 支付服务  │
│  ┌──────┐ │  │  ┌──────┐ │  │  ┌──────┐ │
│  │订单DB │ │  │  │库存DB │ │  │  │支付DB │ │
│  └──────┘ │  │  └──────┘ │  │  └──────┘ │
└──────────┘  └──────────┘  └──────────┘

1.2 分布式事务的核心问题

拆分后,每个服务拥有独立的数据库,一个跨服务的业务操作(如"下单")需要同时操作多个数据库,无法再使用单机数据库事务来保证一致性

典型场景:电商下单

下单流程:
1. 订单服务 → 创建订单记录(订单数据库)
2. 库存服务 → 扣减商品库存(库存数据库)
3. 支付服务 → 扣减用户余额(支付数据库)

要求:三者要么全部成功,要么全部回滚

如果第 1、2 步成功,第 3 步失败,订单和库存已经变更,但支付未完成,数据就处于不一致状态。

1.3 核心理论基石

CAP 定理

由 Eric Brewer 在 2000 年提出,分布式系统无法同时满足以下三点,最多只能满足其中两点:

属性 含义 放弃的后果
C(Consistency)一致性 所有节点在同一时刻看到相同的数据 放弃 C:允许读取到旧数据(最终一致性)
A(Availability)可用性 每个请求都能获得非错误的响应 放弃 A:部分请求可能被阻塞或超时
P(Partition Tolerance)分区容错性 系统在出现网络分区时仍能正常工作 放弃 P:网络分区时系统停止服务(不现实)

关键结论: 在分布式系统中,网络分区是不可避免的,因此 P 必须满足。实际选择是 CP 或 AP

  • CP 系统:发生分区时,牺牲可用性,保证一致性(如 2PC、ZooKeeper)
  • AP 系统:发生分区时,牺牲一致性,保证可用性(如 Saga、TCC 的最终一致性)
BASE 理论

BASE 是 CAP 定理中 AP 方案的实践指导,是对 ACID 的弱化:

属性 含义
BA(Basically Available)基本可用 系统出现故障时允许损失部分可用性,但核心功能仍然可用
S(Soft State)软状态 允许系统存在中间状态,该中间状态不影响系统整体可用性
E(Eventually Consistent)最终一致性 系统中的所有数据副本经过一段时间后,最终达到一致状态

ACID 与 BASE 对比如下:

维度 ACID BASE
一致性 强一致性 最终一致性
隔离性 强隔离 弱隔离
可用性
适用场景 金融、银行等强一致性需求 互联网电商、社交等高并发场景

二、2PC(两阶段提交)

2.1 概述

2PC(Two-Phase Commit,两阶段提交)是最经典的分布式事务协议,由协调者(Coordinator)和多个参与者(Cohort/Participant)组成。顾名思义,它把事务提交过程分为两个阶段:准备阶段提交阶段

2.2 角色定义

角色 职责
协调者(Coordinator) 事务管理器,负责发起、协调和决策事务的提交或回滚
参与者(Participant) 资源管理器(如数据库),负责执行事务的本地操作

2.3 流程详解

阶段一:准备阶段(Prepare Phase / Voting Phase)
协调者                              参与者
  │                                   │
  │──── ① 发送 Prepare 请求 ────────►│
  │                                   │ ② 执行本地事务(写 undo/redo 日志)
  │                                   │    但不提交
  │                                   │ ③ 锁定相关资源
  │◄─── ④ 回复 YES(可提交)or NO(失败)──│
  │                                   │

协调者做的事情:

  1. 向所有参与者发送 Prepare 消息
  2. 等待所有参与者的响应

参与者做的事情:

  1. 收到 Prepare 请求后,执行本地事务操作,记录 undo(回滚日志)和 redo(重做日志)
  2. 事务未提交,资源处于锁定状态
  3. 回复协调者:如果本地事务执行成功,回复 “YES”(可以提交);如果执行失败,回复 “NO”(需要回滚)
阶段二:提交阶段(Commit Phase)

情况 A:所有参与者都回复 YES(提交)

协调者                              参与者
  │                                   │
  │──── ① 发送 Commit 请求 ────────►│
  │                                   │ ② 提交本地事务
  │                                   │ ③ 释放锁资源
  │◄─── ④ 回复 ACK(确认)────────│
  │                                   │

情况 B:任一参与者回复 NO 或超时(回滚)

协调者                              参与者
  │                                   │
  │──── ① 发送 Rollback 请求 ──────►│
  │                                   │ ② 使用 undo 日志回滚
  │                                   │ ③ 释放锁资源
  │◄─── ④ 回复 ACK(确认)────────│
  │                                   │

2.4 优缺点

优点:

优点 说明
原理简单 只需要两个阶段,逻辑清晰,易于理解和实现
强一致性 保证所有参与者要么全部提交,要么全部回滚(原子性)
行业标准 有 XA 规范支撑,多数数据库(MySQL、Oracle、PostgreSQL)都支持 XA 事务

缺点:

缺点 详细说明
同步阻塞 参与者在准备阶段锁定资源后,必须等待协调者指令才能继续,期间资源被锁定,其他事务无法访问,并发性能极差
单点故障 协调者是整个系统的单点,一旦协调者宕机,所有参与者将无限期阻塞,整个系统瘫痪
数据不一致风险 提交阶段中,如果协调者只向部分参与者发送了 Commit 指令后宕机,部分参与者提交了、部分未提交,数据出现不一致
网络开销大 每个事务需要多次网络往返,延迟较高
不适合高并发 因为资源锁定时间长,不适合高并发互联网场景
容错性差 对网络分区和节点故障的容忍能力有限

2.5 当前主流采用情况

2PC/XA 在互联网主流场景中已基本被淘汰,原因如下:

  1. 互联网场景追求高并发、低延迟,2PC 的资源锁定和同步阻塞完全不可接受
  2. 微服务架构下,服务之间通过 REST/RPC 通信,而非直接使用数据库 XA 协议
  3. 长事务场景下锁竞争会导致系统雪崩

仍有小范围使用的场景:

  • 传统企业应用(银行核心系统、ERP 等强一致性需求)
  • 跨数据库操作(同一应用内跨多个数据库实例)
  • 金融机构的某些合规场景

三、3PC(三阶段提交)

3.1 概述

3PC(Three-Phase Commit,三阶段提交)是 2PC 的改进版本,在 2PC 的两个阶段之间插入了一个预提交阶段(Pre-Commit),旨在解决 2PC 的同步阻塞和单点故障问题。同时引入了超时机制,允许参与者在协调者长时间无响应时自行决策。

3.2 流程详解

阶段一:CanCommit(询问阶段)
协调者                              参与者
  │                                   │
  │──── ① 发送 CanCommit 请求 ──────►│
  │                                   │ ② 判断是否可以执行事务
  │                                   │    (不执行事务,仅做可行性判断)
  │◄─── ③ 回复 YES 或 NO ──────────│
  │                                   │

协调者: 向所有参与者发送 CanCommit 请求,询问是否可以执行事务

参与者: 根据自身状态判断能否执行事务(如检查网络状况、资源可用性等),不执行任何事务操作,回复 YES 或 NO

异常处理: 如果任何参与者回复 NO 或超时,协调者直接终止事务

阶段二:PreCommit(预提交阶段)

情况 A:所有参与者回复 YES

协调者                              参与者
  │                                   │
  │──── ① 发送 PreCommit 请求 ──────►│
  │                                   │ ② 执行本地事务(写 undo/redo 日志)
  │                                   │    但不提交
  │                                   │ ③ 锁定相关资源
  │◄─── ④ 回复 ACK 或 NO ──────────│
  │                                   │

情况 B:有参与者回复 NO 或超时

协调者                              参与者
  │                                   │
  │──── ① 发送 Abort(中断)请求 ───►│
  │                                   │ ② 回滚(或无需操作)
  │◄─── ③ 回复 ACK ────────────────│
  │                                   │
阶段三:DoCommit(提交阶段)

情况 A:协调者收到所有 PreCommit 的 ACK

协调者                              参与者
  │                                   │
  │──── ① 发送 DoCommit 请求 ──────►│
  │                                   │ ② 提交本地事务
  │                                   │ ③ 释放锁资源
  │◄─── ④ 回复 ACK ────────────────│
  │                                   │

情况 B:协调者未收到所有 ACK 或超时

协调者                              参与者
  │                                   │
  │──── ① 发送 Rollback 请求 ──────►│
  │                                   │ ② 使用 undo 日志回滚
  │                                   │ ③ 释放锁资源
  │◄─── ④ 回复 ACK ────────────────│
  │                                   │

3.3 核心改进:参与者的超时中断机制

3PC 相比 2PC 最大的改进是:在 DoCommit 阶段,如果参与者长时间没有收到协调者的指令,它不会无限期阻塞,而是会自动提交事务。

参与者超时后的决策逻辑:

在 PreCommit 阶段超时:
  → 参与者不确定协调者是否存活,也不确定其他参与者的状态
  → 安全策略:回滚事务(因为可能还没进入 DoCommit 阶段)

在 DoCommit 阶段超时:
  → 参与者已经收到了 PreCommit 指令,说明协调者已经决定提交
  → 安全策略:自动提交事务(因为其他参与者大概率也收到了 PreCommit)

这个机制解决了 2PC 中协调者宕机后参与者无限阻塞的问题。

3.4 优缺点

优点:

优点 说明
减少阻塞 引入了超时机制,参与者在 DoCommit 阶段超时后可自动提交,不会无限期阻塞
增加容错性 协调者故障后,参与者可以在超时后自行决策,降低了单点故障的影响
PreCommit 缓冲 在准备阶段前增加了 CanCommit 询问阶段,可以提前发现不可用的参与者,减少不必要的资源锁定

缺点:

缺点 详细说明
实现复杂 比 2PC 多了一个阶段,增加了代码复杂度和调试难度
网络开销更大 三个阶段需要至少三次网络往返,延迟比 2PC 更高
数据不一致风险仍存在 在网络分区场景下,如果部分参与者收到 PreCommit 后与协调者失联,它们超时后自动提交;但协调者可能因为未收到 ACK 而发送回滚指令,导致部分提交、部分回滚的不一致
性能差 相比 2PC,多了一个阶段,延迟更高;资源锁定时间仍然较长
假设不可靠 "进入 DoCommit 阶段就自动提交"的假设在网络分区场景下可能不成立,协调者可能决定回滚

3.5 当前主流采用情况

3PC 在实际生产中极少被采用,原因如下:

  1. 虽然理论上解决了 2PC 的阻塞问题,但引入了新的数据不一致风险(网络分区时自动提交可能导致数据不一致)
  2. 增加了网络开销,性能更差
  3. 实现复杂度高,而收益有限
  4. 在互联网场景中,人们更倾向于使用最终一致性的方案(如 Saga、TCC)

3PC 更多是学术和研究价值,作为分布式一致性协议的理论演进路径的一部分,后来被 Paxos/Raft 等共识算法所取代。


四、TCC(Try-Confirm-Cancel)

4.1 概述

TCC(Try-Confirm-Cancel)是一种补偿型的分布式事务方案,由 Pat Helland 在 2007 年提出。TCC 的核心思想是:将每个服务的操作拆分为三个阶段,通过预留资源 + 确认 / 取消来实现最终一致性。

TCC 属于最终一致性方案,不要求资源锁定,而是通过"预留"和"补偿"来保证数据最终一致。

4.2 三个阶段

阶段一:Try(尝试/预留)
Try 阶段:对所有服务进行资源检查和预留

- 订单服务:将订单状态设置为"预创建",冻结库存
- 库存服务:检查库存是否充足,扣减冻结库存(但不减少实际可售库存)
- 支付服务:检查余额是否充足,冻结对应金额

Try 阶段的核心要求:

  1. 完成所有业务检查(一致性检查)
  2. 预留必须的业务资源,但不实际执行核心操作
  3. 预留的资源应保证后续 Confirm 阶段能够成功执行
阶段二:Confirm(确认)
Confirm 阶段:所有 Try 成功后,执行真正的业务操作

- 订单服务:将订单状态从"预创建"改为"已创建"
- 库存服务:从冻结库存中扣除,更新实际库存
- 支付服务:从冻结金额中扣除,更新实际余额

Confirm 阶段的核心要求:

  1. 必须幂等,允许重试
  2. 如果没有异常,Confirm 一定会成功(因为 Try 阶段已经预留了资源)
  3. 如果 Confirm 失败,需要不断重试直到成功
阶段三:Cancel(取消/补偿)
Cancel 阶段:任一 Try 失败时,对所有已 Try 成功的服务进行补偿

- 订单服务:将订单状态从"预创建"改为"已取消"
- 库存服务:释放冻结库存,恢复可售库存
- 支付服务:释放冻结金额,恢复可用余额

Cancel 阶段的核心要求:

  1. 必须幂等,允许重试
  2. 释放 Try 阶段预留的资源
  3. 如果 Cancel 失败,需要不断重试直到成功

4.3 流程示例

以电商下单为例,展示 TCC 的完整流程:

场景:用户下单购买商品,价格为 100 元

┌─────────────────────────────────────────────────────┐
│                    TCC 协调者                         │
└──┬──────────────────┬──────────────────┬────────────┘
   │                  │                  │
   ▼                  ▼                  ▼
┌──────┐          ┌──────┐          ┌──────┐
│订单服务│          │库存服务│          │支付服务│
└──────┘          └──────┘          └──────┘

===== Try 阶段 =====
订单服务: 创建订单(状态=预创建),冻结商品
库存服务: 检查库存>=1,冻结库存 -1
支付服务: 检查余额>=100,冻结余额 100

===== 全部 Try 成功 → Confirm 阶段 =====
订单服务: 订单状态:预创建 → 已创建
库存服务: 冻结库存扣减生效,实际库存 -1
支付服务: 冻结余额扣减生效,实际余额 -100

===== 任一步 Try 失败 → Cancel 阶段 =====
假设:支付服务 Try 失败(余额不足)
订单服务: 取消订单(状态=预创建 → 已取消),释放冻结
库存服务: 释放冻结库存,冻结库存 +1
支付服务: 无需操作(Try 阶段未成功)

4.4 补偿机制(重点)

TCC 的补偿机制是其核心所在,与 2PC 的 undo 日志回滚不同,TCC 的补偿是业务层面的,需要开发者自己实现。

补偿策略
┌────────────────────────────────────────────────┐
│               TCC 补偿机制                       │
├────────────────────────────────────────────────┤
│                                                 │
│  1. 空回滚(Null Rollback)                     │
│     - 场景:Try 请求未到达服务或超时,协调者     │
│       直接发送了 Cancel 指令                    │
│     - 处理:Cancel 接口需要识别到没有对应的 Try  │
│       记录,直接返回成功(空回滚)               │
│     - 实现:通常在数据库记录 Try 操作日志,      │
│       Cancel 时检查日志是否存在                  │
│                                                 │
│  2. 防悬挂(Prevent Suspension)                │
│     - 场景:Cancel 先于 Try 到达(网络乱序),   │
│       之后 Try 再到达                           │
│     - 处理:Try 接口需要检查是否有对应的 Cancel  │
│       记录,如果有则拒绝执行 Try                 │
│     - 实现:Cancel 时记录操作日志,Try 时检查    │
│                                                 │
│  3. 幂等性(Idempotence)                       │
│     - Try/Confirm/Cancel 都可能被重试,必须保证  │
│       多次调用结果一致                           │
│     - 实现:基于唯一事务 ID 做去重判断           │
│                                                 │
│  4. 重试机制(Retry)                           │
│     - Confirm/Cancel 失败后需要不断重试          │
│     - 重试策略:指数退避、固定间隔等             │
│     - 人工兜底:超过最大重试次数后转为人工处理   │
│                                                 │
└────────────────────────────────────────────────┘
补偿代码示例
// 库存服务的 TCC 实现

@Service
public class InventoryTccService {

    // Try:预留库存
    @Transactional
    public boolean tryDeductStock(String txId, String productId, int count) {
        // 防悬挂:检查是否已有 Cancel 记录
        if (tccLogDao.existsCancel(txId)) {
            return false; // 拒绝执行 Try
        }
        // 幂等:检查是否已经 Try 过
        if (tccLogDao.existsTry(txId)) {
            return true; // 已 Try 过,直接返回成功
        }
        // 业务检查
        int frozenStock = productDao.getFrozenStock(productId);
        int availableStock = productDao.getAvailableStock(productId);
        if (availableStock < count) {
            return false; // 库存不足
        }
        // 减少可用库存,增加冻结库存
        productDao.decreaseAvailableStock(productId, count);
        productDao.increaseFrozenStock(productId, count);
        // 记录 Try 日志
        tccLogDao.insertTryLog(txId, productId, count);
        return true;
    }

    // Confirm:确认扣减
    @Transactional
    public boolean confirmDeductStock(String txId, String productId, int count) {
        // 幂等检查
        if (tccLogDao.existsConfirm(txId)) {
            return true;
        }
        // 减少冻结库存,更新总库存
        productDao.decreaseFrozenStock(productId, count);
        productDao.decreaseTotalStock(productId, count);
        // 记录 Confirm 日志
        tccLogDao.insertConfirmLog(txId, productId, count);
        return true;
    }

    // Cancel:释放冻结库存
    @Transactional
    public boolean cancelDeductStock(String txId, String productId, int count) {
        // 空回滚:如果 Try 日志不存在,说明 Try 未执行
        if (!tccLogDao.existsTry(txId)) {
            // 记录 Cancel 日志(防悬挂用)
            tccLogDao.insertCancelLog(txId, productId, count);
            return true; // 空回滚,直接返回成功
        }
        // 幂等检查
        if (tccLogDao.existsCancel(txId)) {
            return true;
        }
        // 恢复可用库存,减少冻结库存
        productDao.increaseAvailableStock(productId, count);
        productDao.decreaseFrozenStock(productId, count);
        // 记录 Cancel 日志
        tccLogDao.insertCancelLog(txId, productId, count);
        return true;
    }
}

4.5 优缺点

优点:

优点 说明
无资源锁定 不像 2PC 那样锁定数据库资源,Try 阶段只做"预留",不阻塞其他事务,并发性能高
最终一致性 通过 Confirm/Cancel 保证数据最终一致,适合高并发互联网场景
业务可控 由业务方自己实现 Try/Confirm/Cancel,可以灵活处理各种业务场景
性能好 没有全局锁,各服务可以并行执行,不依赖单一协调者进行强一致性协调
容错性好 通过重试机制保证 Confirm/Cancel 最终成功,单点故障影响小

缺点:

缺点 详细说明
侵入性强 每个参与的服务都需要实现 Try/Confirm/Cancel 三个接口,对业务代码侵入大
开发成本高 需要处理空回滚、防悬挂、幂等性等复杂问题,开发工作量大
实现难度大 业务逻辑的补偿操作往往比正向操作更复杂,容易出错
维护成本高 业务逻辑变更时,需要同时修改 Try/Confirm/Cancel 三个接口
不适合长事务 如果 Confirm 阶段耗时过长,整个事务时间会拉长,但好在没有锁
数据一致性弱 在 Try 和 Confirm 之间,数据处于中间状态(冻结状态),外部可能读到不一致数据

4.6 当前主流采用情况

TCC 是目前互联网公司中较为主流的分布式事务方案,尤其适用于以下场景:

  1. 金融/支付场景:对资金安全要求高,需要精确的补偿机制
  2. 电商核心交易链路:下单、扣库存、扣款等需要强保证的场景
  3. 跨服务数据一致性场景:微服务架构下需要保证最终一致性的场景

代表实践:

  • 蚂蚁集团:大量使用 TCC 处理支付、转账等核心交易场景
  • 美团、滴滴等:在订单、支付等核心流程中使用 TCC
  • 阿里的 Seata 框架内置了 TCC 模式支持

五、Saga 模式

5.1 概述

Saga 模式最早由 Hector Garcia-Molina 在 1987 年提出,用于解决**长事务(Long-Lived Transaction)**问题。Saga 的核心思想是:将一个长事务拆分为多个本地事务,每个本地事务都有对应的补偿事务,当某个本地事务失败时,按相反顺序执行之前成功事务的补偿操作。

Saga 事务 = 一系列本地事务 + 对应的补偿事务

T1 → T2 → T3 → ... → Tn
C1   C2   C3         Cn

如果 T3 失败:
  → 执行 C2(补偿 T2)→ 执行 C1(补偿 T1)

5.2 两种协调模式

模式一:编排式(Choreography)
┌────────┐  事件A  ┌────────┐  事件B  ┌────────┐
│ 服务A  │────────►│ 服务B  │────────►│ 服务C  │
│ (订单) │◄────────│ (库存) │◄────────│ (支付) │
└────────┘  补偿事件  └────────┘  补偿事件  └────────┘

特点:

  • 每个服务独立监听事件并执行自己的事务
  • 自己决定何时执行本地事务,并发布事件触发下一个服务
  • 失败时发布补偿事件,由上游服务自己处理补偿

优点: 松耦合,服务之间不直接依赖
缺点: 流程分散在各个服务中,难以追踪和理解整体流程

模式二:编排器式(Orchestration)
                    ┌──────────────┐
                    │  Saga 编排器   │
                    │ (Order Saga) │
                    └──┬───┬───┬──┘
                       │   │   │
                  ┌────▼┐ ┌▼───┐ ┌────▼┐
                  │订单服务│ │库存服务│ │支付服务│
                  └──────┘ └─────┘ └──────┘

特点:

  • 由一个 Saga 编排器(Orchestrator)集中管理整个流程
  • 编排器按顺序调用各个服务,并处理失败补偿
  • 流程逻辑集中,便于管理和追踪

优点: 流程集中管理,清晰可控,易于维护
缺点: 编排器成为关键节点,逻辑复杂度集中

目前业界以编排器式为主流,因为它更易于管理和维护。

5.3 流程示例

以电商下单为例,展示 Saga 编排器模式的完整流程:

Saga 编排器:OrderSaga

步骤 1:创建订单
  → 调用订单服务.createOrder()
  → 成功:记录补偿函数 = 订单服务.cancelOrder(orderId)
  → 失败:终止 Saga,无需补偿

步骤 2:扣减库存
  → 调用库存服务.deductStock()
  → 成功:记录补偿函数 = 库存服务.restoreStock(productId, count)
  → 失败:执行补偿步骤 1(取消订单)

步骤 3:扣减余额
  → 调用支付服务.deductBalance()
  → 成功:Saga 完成
  → 失败:执行补偿步骤 2(恢复库存)→ 执行补偿步骤 1(取消订单)

5.4 补偿机制(重点)

Saga 的补偿机制是反向补偿,即按照事务执行的逆序执行补偿操作。

补偿的核心原则
Saga 补偿的三大原则:

1. 逆序补偿(Backward Recovery)
   - 从失败点开始,向前(逆序)依次执行补偿
   - 例如:T1→T2→T3(失败),补偿顺序:C2→C1

2. 补偿幂等
   - 补偿操作可能被重试,必须保证幂等性
   - 同一个补偿操作多次执行,结果一致

3. 补偿必须成功
   - 补偿操作失败后需要不断重试,直到成功
   - 超过重试上限需要人工介入
补偿策略详解
┌──────────────────────────────────────────────────────┐
│                  Saga 补偿策略                         │
├──────────────────────────────────────────────────────┤
│                                                       │
│  1. 正向补偿(Forward Recovery)                      │
│     - 适用场景:失败后可以重试当前步骤(非致命错误)    │
│     - 处理方式:重试失败的步骤,不触发逆序补偿         │
│     - 示例:库存扣减因网络超时失败,重试可能成功       │
│                                                       │
│  2. 反向补偿(Backward Recovery)                     │
│     - 适用场景:当前步骤是业务失败,无法重试           │
│     - 处理方式:逆序执行所有成功步骤的补偿             │
│     - 示例:余额不足,无法完成支付,需要回滚整个订单   │
│                                                       │
│  3. 混合补偿(Mixed Recovery)                        │
│     - 适用场景:部分步骤可重试,部分不可重试           │
│     - 处理方式:先重试可重试的步骤,超时后转为反向补偿  │
│                                                       │
│  4. 隔离性处理(Isolation)                           │
│     - 问题:Saga 是多个独立的本地事务,中间状态        │
│       可能被其他事务读取,导致脏读                     │
│     - 解决方案:                                       │
│       a. 语义锁:在业务字段上标记"处理中"状态          │
│       b. 版本号/乐观锁:更新时检查版本号               │
│       c. 事务间写隔离:确保冲突操作有合理的顺序         │
│                                                       │
└──────────────────────────────────────────────────────┘
补偿代码示例
// Saga 编排器实现

@Component
public class OrderSagaOrchestrator {

    @Autowired
    private OrderService orderService;
    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private PaymentService paymentService;

    @Transactional
    public void executeOrderSaga(OrderRequest request) {
        // 记录 Saga 执行日志
        SagaLog sagaLog = new SagaLog(request.getOrderId());
        sagaLogDao.insert(sagaLog);

        try {
            // 步骤 1:创建订单
            Order order = orderService.createOrder(request);
            sagaLog.addStep("CREATE_ORDER", () -> {
                orderService.cancelOrder(order.getId()); // 补偿函数
            });

            // 步骤 2:扣减库存
            inventoryService.deductStock(request.getProductId(), request.getCount());
            sagaLog.addStep("DEDUCT_STOCK", () -> {
                inventoryService.restoreStock(request.getProductId(), request.getCount());
            });

            // 步骤 3:扣减余额
            paymentService.deductBalance(request.getUserId(), request.getAmount());
            sagaLog.addStep("DEDUCT_BALANCE", () -> {
                paymentService.refundBalance(request.getUserId(), request.getAmount());
            });

            // 全部成功
            sagaLog.setStatus(SagaStatus.COMPLETED);
            sagaLogDao.update(sagaLog);

        } catch (Exception e) {
            // 执行补偿:逆序执行所有已记录的补偿函数
            sagaLog.compensate(); // 从最后一步开始向前补偿
            sagaLog.setStatus(SagaStatus.COMPENSATED);
            sagaLogDao.update(sagaLog);
            throw new SagaCompensationException("Saga 补偿完成", e);
        }
    }
}

// SagaLog 补偿逻辑
public class SagaLog {
    private List<SagaStep> steps = new ArrayList<>();

    public void compensate() {
        // 逆序补偿
        for (int i = steps.size() - 1; i >= 0; i--) {
            SagaStep step = steps.get(i);
            try {
                // 检查是否已补偿(幂等)
                if (!step.isCompensated()) {
                    step.getCompensation().execute(); // 执行补偿
                    step.setCompensated(true);
                }
            } catch (Exception e) {
                // 补偿失败,记录日志,异步重试或人工处理
                compensationLogDao.insert(new CompensationLog(step, e));
                // 继续执行前一个步骤的补偿(尽力补偿)
            }
        }
    }
}

5.5 优缺点

优点:

优点 说明
高可用 没有全局锁,各服务独立执行,不阻塞其他事务,可用性极高
高性能 本地事务执行快,不需要等待其他参与者,并发性能好
适合长事务 不锁定资源,事务可以持续很长时间,适合工作流、审批流等场景
松耦合 编排式模式下,服务之间通过事件异步通信,耦合度低
容错性好 通过补偿机制处理失败,没有单点故障问题

缺点:

缺点 详细说明
缺乏隔离性 Saga 的各步骤是独立的本地事务,中间状态可见,可能导致脏读、不可重复读等问题
补偿实现复杂 需要为每个正向操作编写对应的补偿操作,业务逻辑逆向还原往往很困难
补偿可能失败 补偿操作本身可能失败(如服务宕机),需要重试和人工兜底机制
无自动回滚 不像 2PC 那样数据库自动回滚,需要手动编写所有补偿逻辑
数据一致性弱 只有在所有 Saga 步骤完成后,数据才最终一致,中间可能存在不一致窗口

5.6 与 TCC 的对比

维度 TCC Saga
资源预留 有(Try 阶段预留资源) 无(直接执行)
事务失败处理 Cancel 回滚 逆序补偿
隔离性 较好(Try 预留资源) 较差(执行即生效)
实现复杂度 高(3 个接口) 中(1 个正向 + 1 个补偿)
代码侵入性
适用场景 金融、支付等强一致性需求 长事务、工作流、非核心业务

5.7 当前主流采用情况

Saga 模式是当前微服务架构中最主流的分布式事务方案之一,原因如下:

  1. 互联网追求高可用、高并发,Saga 的无锁特性完美契合
  2. 微服务架构天然适合 Saga 的编排式或编排器式模式
  3. 补偿逻辑虽复杂,但比 TCC 侵入性小,更易于实现
  4. 适合长事务场景,这是其他方案难以解决的

代表实践:

  • Netflix:Conductor 工作流引擎使用 Saga 处理视频编码等长事务
  • Uber:使用 Saga 处理打车订单的生命周期
  • 阿里的 Seata 框架内置 Saga 模式支持
  • 各类微服务框架(如 Spring Cloud、Axon Framework)都提供了 Saga 支持

六、AT 模式

6.1 概述

AT 模式(Automatic Transaction,自动事务)是阿里 Seata 框架独创的分布式事务模式,它是对 2PC 的改进和优化。AT 模式的核心思想是:基于关系型数据库的本地事务,通过自动生成 undo 日志(前镜像和后镜像)实现自动回滚,对业务代码零侵入。

AT 模式本质上是一种改良版的 2PC,它解决了 2PC 的以下问题:

  • 不需要 XA 协议支持,基于普通本地事务
  • 锁粒度更细(全局锁而不是数据库锁)
  • 只在第一阶段短暂的锁定资源

6.2 架构角色

┌──────────────────────────────────────────────────────┐
│                     Seata 架构                        │
├──────────────────────────────────────────────────────┤
│                                                       │
│  ┌──────────┐      ┌──────────┐                      │
│  │ TC(事务   │      │ TM(事务  │                      │
│  │ 协调器)   │◄────►│ 管理器)  │                      │
│  └────┬─────┘      └────┬─────┘                      │
│       │                 │                             │
│       │        ┌────────┴────────┐                    │
│       │        ▼                 ▼                    │
│       │  ┌──────────┐      ┌──────────┐              │
│       └──►│ RM(资源  │      │ RM(资源  │              │
│           │ 管理器)  │      │ 管理器)  │              │
│           └──────────┘      └──────────┘              │
│                                                       │
└──────────────────────────────────────────────────────┘
角色 英文 说明
TC(事务协调器) Transaction Coordinator 独立部署的服务,负责维护全局事务和分支事务的状态,驱动全局提交或回滚
TM(事务管理器) Transaction Manager 定义全局事务的范围:开始全局事务、提交或回滚全局事务
RM(资源管理器) Resource Manager 管理分支事务处理的资源,与 TC 通信以注册分支事务和报告分支事务状态,并驱动分支事务提交或回滚

6.3 流程详解

两阶段流程
第一阶段:执行 + 注册

┌──────────────────────────────────────┐
│ TM:开启全局事务                       │
│   → 向 TC 注册全局事务,获取 XID       │
│   → 将 XID 传播到各个微服务(通过 RPC) │
└──────────────────────────────────────┘
                    │
    ┌───────────────┼───────────────┐
    ▼               ▼               ▼
┌──────┐       ┌──────┐       ┌──────┐
│  RM1  │       │  RM2  │       │  RM3  │
│ 执行SQL│       │ 执行SQL│       │ 执行SQL│
│ 保存前镜像│      │ 保存前镜像│      │ 保存前镜像│
│ 执行SQL│       │ 执行SQL│       │ 执行SQL│
│ 保存后镜像│      │ 保存后镜像│      │ 保存后镜像│
│ 生成undo│       │ 生成undo│       │ 生成undo│
│ 提交本地事务│     │ 提交本地事务│     │ 提交本地事务│
│ 注册分支事务│     │ 注册分支事务│     │ 注册分支事务│
│ 释放本地锁│      │ 释放本地锁│      │ 释放本地锁│
│ 获取全局锁│      │ 获取全局锁│      │ 获取全局锁│
└──────┘       └──────┘       └──────┘
    │               │               │
    └───────────────┼───────────────┘
                    ▼
┌──────────────────────────────────────┐
│ TC:等待所有分支事务注册完成           │
│   → 收到所有注册后,向 TM 报告完成     │
└──────────────────────────────────────┘


第二阶段:全局提交

TC 根据所有分支事务的报告决定全局提交或回滚:

┌──── 全局提交 ────┐     ┌──── 全局回滚 ────┐
│                   │     │                   │
│ TC → RM: 异步删除  │     │ TC → RM: 异步回滚  │
│   undo 日志       │     │   使用 undo 日志   │
│                   │     │   回复到前镜像状态  │
│ RM: 释放全局锁     │     │ RM: 释放全局锁     │
│                   │     │                   │
└───────────────────┘     └───────────────────┘
前镜像与后镜像

AT 模式的核心是自动生成 undo 日志,通过解析 SQL 语句,自动记录数据变更前后的快照:

// 执行 SQL 前,记录前镜像(Before Image)
SELECT id, name, balance FROM account WHERE id = 1;
// 前镜像: {id: 1, name: "张三", balance: 1000}

// 执行 UPDATE
UPDATE account SET balance = balance - 100 WHERE id = 1;

// 执行 SQL 后,记录后镜像(After Image)
SELECT id, name, balance FROM account WHERE id = 1;
// 后镜像: {id: 1, name: "张三", balance: 900}

// 生成的 undo 日志包含:
// 前镜像:{id: 1, name: "张三", balance: 1000}
// 后镜像:{id: 1, name: "张三", balance: 900}
// 回滚时,根据前镜像生成反向 SQL:
// UPDATE account SET balance = 1000 WHERE id = 1

6.4 全局锁与写隔离

AT 模式引入了全局锁的概念来解决并发写冲突:

┌──────────────────────────────────────────────────────┐
│                  AT 模式的写隔离                       │
├──────────────────────────────────────────────────────┤
│                                                       │
│  事务A(持有全局锁)                                    │
│  ┌─────────────────────────────────────────┐          │
│  │ 1. 获取全局锁 ✓                          │          │
│  │ 2. 执行 UPDATE,提交本地事务              │          │
│  │ 3. 注册分支事务                          │          │
│  │ 4. 等待全局事务提交/回滚...               │          │
│  │ 5. 全局事务完成后,释放全局锁             │          │
│  └─────────────────────────────────────────┘          │
│                                                       │
│  事务B(等待全局锁)                                    │
│  ┌─────────────────────────────────────────┐          │
│  │ 1. 尝试获取全局锁 ✗(被事务A持有)        │          │
│  │ 2. 等待全局锁释放...                      │          │
│  │ 3. 全局锁释放后,重新获取                 │          │
│  │ 4. 继续执行                              │          │
│  └─────────────────────────────────────────┘          │
│                                                       │
└──────────────────────────────────────────────────────┘

写隔离机制: 在本地事务提交前,AT 模式会尝试获取该记录的全局锁。如果获取失败,则会等待或重试,直到超时。

6.5 补偿机制(重点)

AT 模式的补偿机制是其核心创新,相比于 2PC 和 TCC,它更加自动化:

自动回滚
AT 模式回滚流程:

1. TC 收到 TM 的全局回滚请求
2. TC 向所有已注册的 RM 发送分支回滚请求
3. RM 收到回滚请求后:
   a. 通过 XID 和 Branch ID 找到对应的 undo 日志
   b. 验证当前数据是否与后镜像一致(数据脏写校验)
   c. 如果不一致,说明数据被其他事务修改,需要特殊处理
   d. 如果一致,根据前镜像生成反向 SQL 执行回滚
   e. 删除 undo 日志
   f. 提交本地事务
4. 向 TC 报告回滚完成
数据脏写校验
数据校验逻辑:

回滚时,RM 会执行:
SELECT * FROM table WHERE id = ? FOR UPDATE  -- 获取当前行数据

比较当前数据与 undo 日志中的后镜像:

情况 1:当前数据 == 后镜像
  → 数据未被其他事务修改,可以安全回滚
  → 执行反向 SQL:UPDATE ... SET balance = 1000 WHERE id = 1

情况 2:当前数据 != 后镜像
  → 数据被其他事务修改(脏写)
  → 无法自动回滚,需要人工处理
  → 策略:记录异常日志,发送告警,人工介入
补偿机制总结
┌──────────────────────────────────────────────────┐
│           AT 模式补偿机制总结                      │
├──────────────────────────────────────────────────┤
│                                                   │
│  1. 自动生成 undo 日志                            │
│     - 无需开发者编写补偿逻辑                       │
│     - 通过解析 SQL 自动生成前镜像和后镜像          │
│                                                   │
│  2. 自动回滚                                      │
│     - 全局回滚时,TC 自动通知所有 RM 执行回滚      │
│     - RM 根据 undo 日志自动生成反向 SQL            │
│                                                   │
│  3. 数据脏写校验                                  │
│     - 回滚前校验当前数据是否与后镜像一致            │
│     - 不一致时拒绝自动回滚,转人工处理              │
│                                                   │
│  4. 全局锁防止脏写                                │
│     - 在本地事务提交前获取全局锁                   │
│     - 全局事务完成前,其他事务无法修改同一行数据    │
│                                                   │
│  5. 补偿的幂等性                                  │
│     - undo 日志根据 XID + Branch ID 唯一标识      │
│     - 重复回滚请求不会产生副作用                   │
│                                                   │
│  6. 异步补偿                                      │
│     - 第二阶段是异步的,不阻塞业务                 │
│     - 提交/回滚失败会重试,直到成功或超时转为人工  │
│                                                   │
└──────────────────────────────────────────────────┘

6.6 优缺点

优点:

优点 说明
零侵入 对业务代码完全无侵入,只需添加一个注解 @GlobalTransactional,加上配置即可使用
自动补偿 无需开发者编写补偿逻辑,框架自动解析 SQL 生成 undo 日志和反向 SQL
高性能 第一阶段提交本地事务后释放本地锁,只持有全局锁,锁粒度更细,并发性能更好
行业标准兼容 基于普通本地事务(JDBC 事务),不依赖 XA 协议,兼容所有主流数据库
易于集成 与 Spring Boot、Spring Cloud、Dubbo 等框架深度集成,开箱即用

缺点:

缺点 详细说明
仅支持关系型数据库 依赖数据库的 ACID 事务和 SQL 解析,不支持 NoSQL、消息队列等
SQL 解析限制 仅支持 INSERT、UPDATE、DELETE 操作,不支持复杂 SQL(如存储过程、多表关联更新)
全局锁开销 第二阶段提交前需要持有全局锁,高并发时可能成为瓶颈
数据一致性风险 第一阶段提交后到第二阶段提交前,数据对外可见(本地事务已提交),存在中间状态
脏写风险 虽然有全局锁,但在极端情况下(如全局锁超时),仍可能出现脏写
依赖 Seata Server 需要独立部署 Seata Server(TC),增加了运维复杂度
数据膨胀 undo_log 表会随着事务量增长而膨胀,需要定期清理

6.7 当前主流采用情况

AT 模式在国内互联网公司中非常流行,尤其是在阿里系生态中广泛使用:

  1. 阿里系:淘宝、天猫、菜鸟等核心业务系统大量使用 AT 模式
  2. 国内互联网公司:美团、滴滴、字节跳动等公司都有使用 Seata AT 模式
  3. 中小型企业:因为 AT 模式零侵入、易用的特性,在中小型企业中非常受欢迎

AT 模式是当前国内分布式事务的事实标准之一,但国际上更倾向于使用 Saga 模式。


七、可靠消息模式

7.1 概述

可靠消息模式(Reliable Message Pattern)是一种基于消息队列实现最终一致性的分布式事务方案。其核心思想是:通过消息中间件异步传递事务指令,上游服务先完成本地事务,再通过可靠的机制将消息投递给下游服务,保证上下游数据最终一致。

可靠消息模式不要求两阶段提交和回滚,而是通过"正向确认 + 重试"保证消息一定被消费,下游服务消费消息时通过幂等设计保证数据正确。

┌──────────┐  ① 发送半消息    ┌──────────┐
│  上游服务  │ ───────────────► │ 消息中间件  │
│  (订单)   │◄─────────────── │ (RocketMQ) │
└────┬─────┘  ② 半消息确认     └────┬─────┘
     │                              │
     │ ③ 执行本地事务               │ ④ 提交/回滚消息
     │   订单入库                    │
     │                              │
     │                              ▼
     │                        ┌──────────┐
     └────────────────────────│ 下游服务  │
                              │  (库存)   │
                              │ ⑤ 消费消息 │
                              │ ⑥ 扣减库存 │
                              └──────────┘

7.2 三种实现方案

可靠消息模式有三种主流实现方案:

方案一:本地消息表(Local Message Table)

原理: 在业务数据库中额外创建一张"本地消息表",将业务操作和消息记录放在同一个本地事务中,保证两者原子性。然后通过定时任务扫描消息表,将消息发送到消息队列。

┌────────────────────────────────────────────────────────────────┐
│                    本地消息表方案流程                             │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  上游服务:                                                       │
│  ┌──────────────────────────────────────────────────┐           │
│  │ 数据库事务(原子性保证)                            │           │
│  │  1. 执行业务操作(如创建订单)                      │           │
│  │  2. 插入本地消息表记录(状态=待发送)                │           │
│  │  3. 提交事务                                      │           │
│  └──────────────────────────────────────────────────┘           │
│                                                                 │
│  定时任务(独立线程/进程):                                       │
│  ┌──────────────────────────────────────────────────┐           │
│  │ 1. 扫描本地消息表,查询状态=待发送的记录            │           │
│  │ 2. 发送消息到 MQ                                 │           │
│  │ 3. 发送成功 → 更新状态=已发送                      │           │
│  │ 4. 发送失败 → 重试(指数退避)                     │           │
│  └──────────────────────────────────────────────────┘           │
│                                                                 │
│  下游服务:                                                       │
│  ┌──────────────────────────────────────────────────┐           │
│  │ 1. 消费 MQ 消息                                  │           │
│  │ 2. 幂等性检查(根据消息ID判断是否已处理)           │           │
│  │ 3. 执行本地事务(如扣减库存)                      │           │
│  │ 4. 消费成功 → 确认消息                            │           │
│  │ 5. 消费失败 → 重试或进入死信队列                   │           │
│  └──────────────────────────────────────────────────┘           │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

本地消息表结构:

CREATE TABLE local_message (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    message_id VARCHAR(64) NOT NULL UNIQUE COMMENT '消息唯一ID',
    topic VARCHAR(128) NOT NULL COMMENT '消息主题',
    tag VARCHAR(128) COMMENT '消息标签',
    message_body TEXT NOT NULL COMMENT '消息体(JSON)',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待发送, 1-发送成功, 2-发送失败',
    retry_count INT DEFAULT 0 COMMENT '重试次数',
    max_retry INT DEFAULT 10 COMMENT '最大重试次数',
    next_retry_time DATETIME COMMENT '下次重试时间',
    create_time DATETIME NOT NULL,
    update_time DATETIME NOT NULL,
    INDEX idx_status_next_retry (status, next_retry_time)
);

代码示例:

@Service
public class OrderService {

    @Autowired
    private OrderDao orderDao;
    @Autowired
    private LocalMessageDao messageDao;

    // 创建订单 + 记录本地消息(同一个事务)
    @Transactional
    public void createOrder(OrderRequest request) {
        // 1. 业务操作:创建订单
        Order order = new Order();
        order.setOrderId(generateOrderId());
        order.setStatus("CREATED");
        orderDao.insert(order);

        // 2. 记录本地消息(与业务操作在同一事务中)
        LocalMessage message = new LocalMessage();
        message.setMessageId(generateMessageId());
        message.setTopic("ORDER_CREATED");
        message.setMessageBody(buildMessageBody(order));
        message.setStatus(0); // 待发送
        messageDao.insert(message);
    }
}

// 定时任务:扫描并发送消息
@Component
public class MessageSendTask {

    @Scheduled(fixedDelay = 5000) // 每 5 秒执行一次
    public void sendMessages() {
        // 查询待发送的消息
        List<LocalMessage> messages = messageDao.findByStatus(0, 100);
        for (LocalMessage msg : messages) {
            try {
                // 发送到 MQ
                rocketMQTemplate.send(msg.getTopic(), msg.getMessageBody());
                // 更新状态为发送成功
                messageDao.updateStatus(msg.getMessageId(), 1);
            } catch (Exception e) {
                // 更新重试信息
                messageDao.incrementRetry(msg.getMessageId(), calculateNextRetryTime(msg.getRetryCount()));
                log.error("消息发送失败: {}", msg.getMessageId(), e);
            }
        }
    }
}

// 下游服务:消费消息(幂等处理)
@Component
@RocketMQMessageListener(topic = "ORDER_CREATED", consumerGroup = "inventory_group")
public class InventoryConsumer implements RocketMQListener<String> {

    @Autowired
    private InventoryService inventoryService;

    @Override
    public void onMessage(String message) {
        OrderEvent event = JSON.parseObject(message, OrderEvent.class);
        String messageId = event.getMessageId();

        // 幂等性检查:是否已处理过
        if (inventoryService.isProcessed(messageId)) {
            return; // 已处理,直接返回
        }

        // 执行业务操作
        inventoryService.deductStock(event.getProductId(), event.getCount());

        // 标记已处理
        inventoryService.markProcessed(messageId);
    }
}
方案二:事务消息(Transactional Message)

原理: 利用消息中间件提供的事务消息能力(如 RocketMQ 的事务消息、Kafka 的事务消息),将消息发送分为两个阶段:先发送半消息占位,等本地事务执行完后,再确认提交或回滚。

┌────────────────────────────────────────────────────────────────┐
│                 RocketMQ 事务消息流程                             │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ① 发送半消息(Half Message)                                    │
│     ┌──────────┐         ┌──────────┐                          │
│     │ Producer │────────►│ RocketMQ  │                          │
│     │          │         │  (半消息不可消费)│                      │
│     └──────────┘         └──────────┘                          │
│                                                                 │
│  ② 执行本地事务                                                   │
│     ┌──────────┐                                                │
│     │ Producer │ ──► 执行业务操作(如创建订单)                    │
│     │          │ ──► 成功或失败                                   │
│     └──────────┘                                                │
│                                                                 │
│  ③ 提交/回滚半消息                                                │
│     ┌──────────┐  成功→COMMIT   ┌──────────┐                   │
│     │ Producer │ ──────────────►│ RocketMQ  │                   │
│     │          │◄──────────────│          │                   │
│     └──────────┘  失败→ROLLBACK │  消息可消费│                   │
│                                 └──────────┘                   │
│                                                                 │
│  ④ 事务回查(如果步骤③因网络问题未到达 MQ)                         │
│     ┌──────────┐  回查本地事务状态  ┌──────────┐                 │
│     │ RocketMQ │ ────────────────►│ Producer │                 │
│     │          │◄────────────────│          │                 │
│     └──────────┘  返回提交/回滚     └──────────┘                 │
│                                                                 │
│  ⑤ 消费者消费消息                                                 │
│     ┌──────────┐         ┌──────────┐                          │
│     │ Consumer │◄────────│ RocketMQ  │                          │
│     │          │         │  (已提交消息)│                          │
│     └──────────┘         └──────────┘                          │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

RocketMQ 事务消息代码示例:

// 生产者:发送事务消息
@Service
public class OrderTransactionProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void sendOrderCreatedMessage(Order order) {
        // 构建消息
        Message<String> message = MessageBuilder
            .withPayload(JSON.toJSONString(order))
            .build();

        // 发送事务消息
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
            "order_tx_group",           // 事务生产者组
            "ORDER_CREATED",            // Topic
            message,                    // 消息
            order                       // 附加参数,传给 executeLocalTransaction
        );

        if (result.getSendStatus() != SendStatus.SEND_OK) {
            throw new RuntimeException("事务消息发送失败");
        }
    }
}

// 事务监听器:执行本地事务 + 本地事务回查
@RocketMQTransactionListener(txProducerGroup = "order_tx_group")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {

    @Autowired
    private OrderService orderService;

    // 执行本地事务
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        Order order = (Order) arg;
        try {
            // 执行本地事务:创建订单
            orderService.createOrder(order);
            // 本地事务成功 → 提交消息
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            // 本地事务失败 → 回滚消息
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    // 本地事务回查(MQ 长时间未收到 COMMIT/ROLLBACK 时触发)
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String orderId = JSON.parseObject((String) msg.getPayload(), Order.class).getOrderId();
        // 查询本地事务执行结果
        Order order = orderService.getOrder(orderId);
        if (order != null && "CREATED".equals(order.getStatus())) {
            // 本地事务已成功执行 → 提交消息
            return RocketMQLocalTransactionState.COMMIT;
        } else {
            // 本地事务未执行或失败 → 回滚消息
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}
方案三:最大努力通知(Best Effort Notification)

原理: 上游服务完成本地事务后,通过消息队列或 HTTP 回调通知下游服务,下游服务处理失败时,上游服务会按一定策略(递增间隔、最大次数)多次重试通知,直到下游处理成功或达到最大重试次数。

┌────────────────────────────────────────────────────────────────┐
│                  最大努力通知流程                                 │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  上游服务完成本地事务                                              │
│    │                                                            │
│    ▼                                                            │
│  发送通知到下游服务                                                │
│    │                                                            │
│    ├── 成功 → 结束                                               │
│    │                                                            │
│    └── 失败 → 重试(间隔递增)                                    │
│         │                                                       │
│         ├── 第 1 次重试:1 分钟后                                 │
│         ├── 第 2 次重试:5 分钟后                                 │
│         ├── 第 3 次重试:10 分钟后                                │
│         ├── 第 4 次重试:30 分钟后                                │
│         ├── 第 5 次重试:1 小时后                                 │
│         │                                                       │
│         └── 超过最大重试次数 → 人工介入 + 告警                     │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

典型应用场景: 支付回调通知、短信验证码发送、异步任务结果通知

// 最大努力通知实现
@Component
public class BestEffortNotificationService {

    @Autowired
    private RestTemplate restTemplate;

    // 发送通知,失败后异步重试
    public void notify(String callbackUrl, NotificationPayload payload) {
        try {
            restTemplate.postForEntity(callbackUrl, payload, String.class);
        } catch (Exception e) {
            // 异步重试
            retryTask.asyncRetry(callbackUrl, payload, 0);
        }
    }
}

@Component
public class RetryTask {

    private static final int[] RETRY_INTERVALS = {60, 300, 600, 1800, 3600}; // 秒
    private static final int MAX_RETRY = 5;

    @Async
    public void asyncRetry(String callbackUrl, NotificationPayload payload, int retryCount) {
        if (retryCount >= MAX_RETRY) {
            // 超过最大重试次数,记录异常,人工处理
            alertService.sendAlert("通知失败,需人工处理", callbackUrl, payload);
            return;
        }

        // 等待指定间隔后重试
        Thread.sleep(RETRY_INTERVALS[retryCount] * 1000);

        try {
            restTemplate.postForEntity(callbackUrl, payload, String.class);
            // 成功,结束
        } catch (Exception e) {
            // 继续重试
            asyncRetry(callbackUrl, payload, retryCount + 1);
        }
    }
}

7.3 三种方案对比

维度 本地消息表 事务消息(RocketMQ) 最大努力通知
实现复杂度 低(MQ 原生支持)
可靠性
实时性 中(依赖定时任务间隔) 高(准实时) 低(依赖重试间隔)
消息中间件依赖 低(任意 MQ) 高(需支持事务消息的 MQ) 低(任意 MQ 或 HTTP)
额外存储 需要(消息表) 不需要 需要(重试记录表)
适用场景 通用场景 需要高实时性的场景 允许一定延迟的异步通知

7.4 补偿机制(重点)

可靠消息模式的补偿机制与前面几种方案有本质区别:它不依赖回滚,而是依赖"正向重试"保证最终一致性。

核心补偿策略
┌──────────────────────────────────────────────────────────────┐
│              可靠消息模式补偿机制                              │
├──────────────────────────────────────────────────────────────┤
│                                                               │
│  1. 消息发送失败补偿                                           │
│     - 本地消息表:定时任务扫描未发送消息,持续重试               │
│     - 事务消息:MQ 通过事务回查确保消息被提交或回滚              │
│     - 最大努力通知:按递增间隔重试,直到成功或达到上限           │
│                                                               │
│  2. 消息消费失败补偿                                           │
│     - MQ 消费重试:consumer 返回失败时,MQ 自动重试             │
│     - 死信队列:重试超过上限后进入死信队列,人工处理             │
│     - 幂等性保证:消费端通过幂等性设计,保证重复消费不会         │
│       产生副作用                                               │
│                                                               │
│  3. 上游已成功、下游未成功的补偿                                │
│     - 这是可靠消息模式的核心补偿场景                             │
│     - 上游事务已提交,但下游消费失败或未消费到消息               │
│     - 补偿方式:通过重试投递 + 死信队列 + 人工对账              │
│                                                               │
│  4. 人工对账补偿(终极兜底)                                    │
│     - 定时对账:通过定时任务对比上下游数据差异                   │
│     - 数据修复:发现差异后自动或人工修复数据                     │
│     - 告警通知:异常数据通过告警通知运维人员                     │
│                                                               │
└──────────────────────────────────────────────────────────────┘
幂等性设计(关键)

可靠消息模式中,幂等性设计是补偿机制的核心。因为消息可能被重复投递,消费端必须保证幂等。

幂等性实现方案:

1. 唯一约束法(数据库方案)
   ┌──────────────────────────────────────────────────┐
   │  CREATE TABLE inventory_deduction_log (           │
   │    id BIGINT AUTO_INCREMENT PRIMARY KEY,          │
   │    message_id VARCHAR(64) NOT NULL UNIQUE,  ← 唯一约束 │
   │    product_id VARCHAR(64),                        │
   │    count INT,                                     │
   │    create_time DATETIME                           │
   │  );                                               │
   │                                                   │
   │  消费逻辑:                                        │
   │  1. INSERT INTO inventory_deduction_log (message_id, ...) │
   │  2. 如果 INSERT 成功(无重复),执行业务操作         │
   │  3. 如果 INSERT 失败(唯一约束冲突),说明已处理,跳过│
   └──────────────────────────────────────────────────┘

2. Redis 去重法
   ┌──────────────────────────────────────────────────┐
   │  String key = "msg:deduction:" + messageId;       │
   │  Boolean exists = redis.setIfAbsent(key, "1",     │
   │                      24, TimeUnit.HOURS);          │
   │  if (!exists) {                                   │
   │      return; // 已处理过,跳过                      │
   │  }                                                │
   │  // 执行业务操作                                    │
   └──────────────────────────────────────────────────┘

3. 版本号法(乐观锁)
   ┌──────────────────────────────────────────────────┐
   │  UPDATE inventory                                │
   │  SET stock = stock - #{count},                   │
   │      version = version + 1                       │
   │  WHERE product_id = #{productId}                 │
   │    AND version = #{expectedVersion}              │
   │                                                  │
   │  如果 affected_rows == 0,说明版本已变,已处理过   │
   └──────────────────────────────────────────────────┘
死信队列与补偿
消息消费失败 → 自动重试(1~16次,指数退避)

  ├── 重试成功 → 消费确认

  └── 重试全部失败 → 进入死信队列(Dead Letter Queue)
       │
       ├── 监控告警:通知运维人员
       │
       ├── 人工排查:分析失败原因
       │   ├── 业务逻辑错误 → 修复代码 → 重新消费
       │   ├── 数据依赖问题 → 修复数据 → 重新消费
       │   └── 临时故障 → 手动重试即可
       │
       └── 数据对账:定时任务对比上下游数据,修复差异

7.5 优缺点

优点:

优点 说明
高可用 上游和下游通过消息队列异步解耦,两者互不阻塞,可用性极高
高性能 异步处理,不锁定资源,吞吐量极高,适合高并发场景
松耦合 上下游服务仅通过消息交互,不直接依赖,易于独立扩展和部署
实现简单 不需要实现 Try/Confirm/Cancel 或补偿逻辑,只需保证消息可靠投递和幂等消费
流量削峰 消息队列天然支持流量削峰填谷,应对突发流量

缺点:

缺点 详细说明
实时性差 异步处理,数据存在延迟,不适合需要实时一致性的场景
数据不一致窗口 消息投递到消费处理之间存在时间窗口,期间数据可能不一致
下游失败处理复杂 下游消费失败后,只能重试或人工处理,无法"回滚"上游已提交的事务
依赖消息中间件 需要额外部署和运维消息队列,增加了系统复杂度
消息丢失风险 极端情况下(如 MQ 故障、磁盘损坏),消息可能丢失,需要额外保障
幂等性要求高 所有消费端必须实现幂等,否则重复消息会导致数据错误
排查困难 异步链路长,涉及多个组件,出问题时排查链路复杂

7.6 当前主流采用情况

可靠消息模式是互联网公司中应用最广泛的分布式事务方案之一,原因如下:

  1. 技术门槛低:不需要实现复杂的分布式事务协议,只需消息队列 + 幂等性设计
  2. 与异步架构天然契合:微服务广泛使用消息队列进行异步通信,可靠消息模式是自然延伸
  3. 性能优异:异步解耦,不阻塞,支持高并发、大流量

代表实践:

  • 几乎所有互联网公司都在使用:订单创建后异步通知各下游服务
  • RocketMQ 事务消息:阿里巴巴、字节跳动等广泛使用
  • Kafka 事务消息:国际公司偏好使用 Kafka 实现类似能力
  • 本地消息表:老系统改造或无法使用事务消息的场景

典型应用场景:

  • 订单创建 → 通知库存服务扣减库存
  • 用户注册 → 发送欢迎邮件/短信
  • 支付成功 → 通知订单系统更新状态
  • 数据变更 → 通知搜索引擎更新索引

八、阿里 Seata 分布式事务框架

8.1 概述

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,前身是阿里内部的 TXC(Taobao Transaction Constructor)和 GTS(Global Transaction Service),于 2019 年 1 月正式开源。

Seata 提供了一套高性能、零侵入、一站式的分布式事务解决方案,支持 AT、TCC、Saga 和 XA 四种事务模式。

8.2 核心架构

┌──────────────────────────────────────────────────────────────┐
│                       Seata 整体架构                           │
├──────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │                    Seata Server(TC)                   │    │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐            │    │
│  │  │ 事务协调   │  │ 锁管理    │  │ 会话管理  │            │    │
│  │  └──────────┘  └──────────┘  └──────────┘            │    │
│  │                                                       │    │
│  │  存储模式:File / DB / Redis                           │    │
│  │  集群部署:支持高可用                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│       ▲                          ▲                           │
│       │                          │                           │
│  ┌────┴────────┐          ┌──────┴──────────┐               │
│  │  服务A(TM+RM)│          │  服务B(RM)      │               │
│  │              │          │                  │               │
│  │ @GlobalTransactio│      │ 执行SQL          │               │
│  │    nal       │          │ 注册分支事务      │               │
│  │ 调用服务B     │────────►│ 报告状态          │               │
│  │              │  RPC     │                  │               │
│  └──────────────┘          └──────────────────┘               │
│                                                               │
└──────────────────────────────────────────────────────────────┘

8.3 四种事务模式详解

模式一:AT 模式(默认)

原理: 基于本地事务 + 自动生成 undo 日志的两阶段提交

使用方式:

// 只需加一个注解,零侵入
@GlobalTransactional(timeoutMills = 300000, name = "create-order")
public void createOrder(OrderRequest request) {
    // 业务逻辑:操作多个数据库
    orderService.save(order);      // 订单服务
    inventoryService.deduct(sku);  // 库存服务
    accountService.deduct(money);  // 账户服务
}

适用场景: 数据库操作、强一致性需求、希望零侵入的业务

模式二:TCC 模式

原理: 用户自定义 Try/Confirm/Cancel 接口

使用方式:

// 定义 TCC 接口
@LocalTCC
public interface AccountTccService {

    @TwoPhaseBusinessAction(
        name = "deductAccount",
        commitMethod = "commit",
        rollbackMethod = "rollback"
    )
    boolean prepare(@BusinessActionContextParameter(paramName = "userId") String userId,
                    @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);

    boolean commit(BusinessActionContext context);

    boolean rollback(BusinessActionContext context);
}

适用场景: 对一致性要求极高的场景(如金融、支付)、需要精确控制资源预留的场景

模式三:Saga 模式

原理: 编排器式 Saga,长事务拆分为多个本地事务,失败时逆序补偿

使用方式: 通过状态机 DSL(JSON)定义 Saga 流程

{
  "Name": "orderSaga",
  "Type": "Saga",
  "StartState": "CreateOrder",
  "States": {
    "CreateOrder": {
      "Type": "ServiceTask",
      "ServiceName": "orderService.createOrder",
      "CompensateState": "CancelOrder",
      "Next": "DeductStock"
    },
    "CancelOrder": {
      "Type": "ServiceTask",
      "ServiceName": "orderService.cancelOrder",
      "End": true
    },
    "DeductStock": {
      "Type": "ServiceTask",
      "ServiceName": "inventoryService.deductStock",
      "CompensateState": "RestoreStock",
      "Next": "DeductBalance"
    },
    "RestoreStock": {
      "Type": "ServiceTask",
      "ServiceName": "inventoryService.restoreStock",
      "Next": "CancelOrder"
    },
    "DeductBalance": {
      "Type": "ServiceTask",
      "ServiceName": "paymentService.deductBalance",
      "CompensateState": "RefundBalance",
      "End": true
    },
    "RefundBalance": {
      "Type": "ServiceTask",
      "ServiceName": "paymentService.refundBalance",
      "Next": "RestoreStock"
    }
  }
}

适用场景: 长事务流程(如订单流程、审批流程)、需要异步处理、老系统改造(无需改造接口)

模式四:XA 模式

原理: 基于 XA 协议的 2PC,使用数据库的 XA 事务支持

使用方式:

# 配置文件中指定模式为 XA
seata.data-source-proxy-mode=XA
@GlobalTransactional
public void createOrder() {
    // 业务逻辑
    // Seata 自动使用 XA 协议协调数据库事务
}

适用场景: 对一致性要求极高(CP 系统)、需要跨数据库厂商的标准化方案、传统企业应用迁移

8.4 四种模式对比

维度 AT 模式 TCC 模式 Saga 模式 XA 模式
一致性 最终一致 最终一致 最终一致 强一致
隔离性 读已提交 业务层隔离 无隔离 全局锁隔离
侵入性 零侵入 高侵入 中侵入 零侵入
性能
补偿方式 自动(undo 日志) 手动(Cancel 接口) 手动(补偿接口) 自动(数据库)
数据库要求 关系型数据库 无限制 无限制 支持 XA 的数据库
开发成本
运维成本 中(需 Seata Server) 中(需 Seata Server) 中(需 Seata Server) 中(需 Seata Server)
适用场景 通用业务 金融/支付 长事务/老系统 强一致性场景

8.5 Seata 的补偿机制(重点)

Seata 为不同模式提供了不同的补偿机制:

AT 模式补偿
AT 模式补偿 = 自动 undo 日志回滚

1. 执行 SQL 时自动生成 undo 日志(前镜像 + 后镜像)
2. 全局回滚时,TC 通知各 RM 执行回滚
3. RM 根据 undo 日志生成反向 SQL
4. 回滚前校验数据一致性(后镜像匹配)
5. 回滚后删除 undo 日志

补偿特点:
- 自动补偿,无需开发者编码
- 有数据脏写校验
- 补偿失败时转为人工处理
TCC 模式补偿
TCC 模式补偿 = 用户自定义 Cancel 方法

1. 协调者调用各服务的 Cancel 接口
2. 开发者需要处理:
   - 空回滚(Try 未执行时收到 Cancel)
   - 防悬挂(Cancel 先于 Try 到达)
   - 幂等性(Cancel 可能被重试)
3. Cancel 失败后重试,超时后人工处理

补偿特点:
- 业务层补偿,精确可控
- 需要处理空回滚、防悬挂、幂等
- 开发成本高但灵活性好
Saga 模式补偿
Saga 模式补偿 = 逆序补偿 + 正向重试

1. 事务失败时,按逆序执行补偿
2. 补偿失败后重试,可配置重试策略
3. 支持正向重试(非致命错误时重试当前步骤)
4. 支持状态机驱动的自动补偿

补偿特点:
- 逆序执行补偿,保证事务完整性
- 支持状态机驱动的自动补偿
- 需要为每个正向操作编写补偿逻辑
XA 模式补偿
XA 模式补偿 = 数据库 XA 协议回滚

1. 与 2PC 相同,使用数据库的 XA 事务
2. 回滚由数据库自动完成
3. 无需 Seata 额外处理补偿逻辑

补偿特点:
- 数据库自动回滚,最可靠
- 但性能最差,锁定时间长

8.6 Seata 的使用步骤

# 1. 引入依赖
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>

# 2. 配置 Seata
seata:
  tx-service-group: my_tx_group
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
  config:
    type: nacos
    nacos:
      server-addr: localhost:8848

# 3. 配置 undo_log 表(AT 模式必须)
CREATE TABLE undo_log (
  id BIGINT NOT NULL AUTO_INCREMENT,
  branch_id BIGINT NOT NULL,
  xid VARCHAR(128) NOT NULL,
  context VARCHAR(128) NOT NULL,
  rollback_info LONGBLOB NOT NULL,
  log_status INT NOT NULL,
  log_created DATETIME NOT NULL,
  log_modified DATETIME NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY ux_undo_log (xid, branch_id)
);

# 4. 业务代码中使用
@GlobalTransactional
public void createOrder(OrderRequest request) {
    // 正常业务逻辑
}

8.7 Seata 的优缺点

优点:

优点 说明
多模式支持 同时支持 AT、TCC、Saga、XA 四种模式,覆盖不同业务场景
零侵入(AT/XA) AT 和 XA 模式对业务代码零侵入,只需一个注解
高性能 AT 模式在第一阶段提交本地事务后释放本地锁,性能优于 2PC
生态完善 与 Spring Cloud、Dubbo、Nacos 等阿里生态深度集成
社区活跃 开源项目,社区活跃,版本迭代快,文档完善
企业级验证 在阿里内部经历了双十一等大流量考验

缺点:

缺点 说明
需要独立部署 Seata Server 增加了运维成本和复杂度,Seata Server 本身也是单点(需集群部署)
SQL 解析限制 AT 模式仅支持 INSERT、UPDATE、DELETE,不支持复杂 SQL
仅支持关系型数据库 AT 模式依赖数据库事务,不支持 NoSQL
全局锁开销 高并发下全局锁可能成为瓶颈
学习成本 配置项较多,概念较多,需要时间学习和调试
版本兼容性 版本升级时可能有 breaking changes,需要关注兼容性

8.8 当前主流采用情况

Seata 是目前国内最主流的分布式事务框架,原因如下:

  1. 阿里背书:经过阿里内部大规模验证,可靠性有保障
  2. 多模式覆盖:AT 模式零侵入适合快速接入,TCC 模式适合高要求场景,Saga 模式适合长事务
  3. 生态完善:与 Spring Cloud Alibaba、Dubbo、Nacos 等无缝集成
  4. 社区活跃:GitHub 上 25k+ Star,持续更新维护

国际对比:

特性 Seata Atomikos Narayana Bitronix
模式 AT/TCC/Saga/XA XA XA/Saga XA
零侵入 ✅(AT/XA)
社区 活跃 一般 一般 已停止维护
国内使用 广泛 极少 极少

九、总结对比

9.1 各方案对比总表

维度 2PC 3PC TCC Saga AT 模式 可靠消息
一致性 强一致 强一致 最终一致 最终一致 最终一致 最终一致
隔离性 全局锁 全局锁 业务层 全局锁+本地锁
性能 极高
可用性 极高
侵入性
实现复杂度 低(框架实现)
补偿方式 自动 自动+超时 手动Cancel 手动补偿 自动undo 正向重试+对账
资源锁定 长时间 长时间 短时间(全局锁)
实时性
长事务支持
当前主流度 极低 高(国内) 极高

9.2 选型建议

┌──────────────────────────────────────────────────────────────────┐
│                      分布式事务选型决策树                          │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  需要强一致性?                                                    │
│  ├── 是 → 2PC/XA 或 3PC                                           │
│  │   ├── 可接受性能差 → 2PC/XA                                     │
│  │   └── 需要减少阻塞 → 3PC                                        │
│  │                                                                │
│  └── 否 → 最终一致性即可                                           │
│      ├── 需要实时一致性?                                          │
│      │   ├── 是 → 同步方案                                         │
│      │   │   ├── 希望零侵入?                                      │
│      │   │   │   ├── 是 → AT 模式(Seata)                         │
│      │   │   │   └── 否 → 继续判断                                 │
│      │   │   ├── 金融/支付场景?                                    │
│      │   │   │   ├── 是 → TCC 模式                                 │
│      │   │   │   └── 否 → 继续判断                                 │
│      │   │   └── 长事务/工作流/老系统改造?                          │
│      │   │       ├── 是 → Saga 模式                                │
│      │   │       └── 否 → AT 模式(默认推荐)                       │
│      │   │                                                        │
│      │   └── 否 → 可接受异步 + 延迟                                  │
│      │       ├── 已有 MQ 基础设施 → 事务消息(RocketMQ)             │
│      │       ├── 无 MQ 或简单场景 → 本地消息表                      │
│      │       └── 回调通知场景 → 最大努力通知                        │
│      │                                                            │
│      └── 混合场景 → Seata 多模式 或 可靠消息 + TCC 组合              │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘
场景 推荐方案 理由
通用互联网业务 Seata AT 模式 零侵入、高性能、易用
异步解耦场景 可靠消息模式 高性能、松耦合、天然适合微服务
金融/支付核心链路 TCC 模式 业务层精确控制,补偿可靠
长事务/审批流程 Saga 模式 无锁、适合长时间运行
传统企业应用 2PC/XA 强一致性、数据库原生支持
老系统改造 可靠消息(本地消息表)或 Saga 无需改造现有接口
高并发削峰 可靠消息模式 异步处理,消息队列天然削峰
混合场景 Seata 多模式 根据业务需要选择不同模式

9.3 最终建议

  1. 大多数互联网业务场景:优先选择 Seata AT 模式,零侵入、高性能,能满足 90% 的业务需求
  2. 异步解耦/高并发削峰:优先选择 可靠消息模式,利用消息队列实现高性能异步处理
  3. 资金安全要求高的场景:选择 TCC 模式,虽然开发成本高,但对资金的精确控制是必要的
  4. 长事务/工作流流程:选择 Saga 模式,无锁设计适合长时间运行的事务
  5. 避免使用 2PC/3PC:除非是传统企业应用或对强一致性有硬性要求,否则不建议使用
  6. 关键原则:能用 AT 就用 AT,需要异步解耦用可靠消息,不行再考虑 TCC 或 Saga,尽量避免 2PC

文档版本: v1.1
更新日期: 2026 年 7 月

更多推荐