跳至主要內容

分布式事务实战:Saga、TCC、AT 模式对比与选型

郑天祺大约 10 分钟分布式分布式事务SagaTCCAT模式Seata

前言

单体应用时代,@Transactional 一行注解就能搞定事务。但拆成微服务后,一个业务可能横跨多个数据库、多个服务,分布式事务就成了绕不过去的坎。

网上讲分布式事务的文章铺天盖地,但大多数只讲理论。本文将结合担保业务系统的实际场景,深度对比 Saga、TCC、AT 三种主流分布式事务模式,给出选型决策树和落地方案。


一、为什么分布式事务这么难?

1.1 单机事务的局限性

在单体应用中,事务管理依赖数据库的 ACID 特性:

@Service
public class CreditService {
    @Autowired
    private CustomerMapper customerMapper;
    @Autowired
    private ContractMapper contractMapper;

    @Transactional
    public void applyCredit(CreditApplyRequest request) {
        customerMapper.insert(request.getCustomer());
        contractMapper.insert(request.getContract());
        // 要么都成功,要么都失败
    }
}

两个表在同一数据库内,通过 InnoDB 的 redolog + binlog 天然保证一致性。

1.2 微服务架构的问题

当 Customer 服务和 Contract 服务分别有自己的数据库时:

Customer 服务 (DB: customer_db)  ──RPC──>  Contract 服务 (DB: contract_db)
     insert customer                       insert contract

此时本地事务无法跨数据库工作。如果 customer 插入成功、contract 插入失败,数据就分裂了。

分布式事务的核心矛盾:如何在多个独立数据库之间保证数据一致性,同时不牺牲系统可用性和性能?

1.3 CAP 视角下的选择

方案一致性可用性复杂度
Saga最终一致性
TCC强一致性(阶段内)
AT最终一致性

二、Saga 模式:最简单但最受限

2.1 原理

Saga 将长事务拆分为一系列本地短事务,每个步骤都有对应的补偿操作

步骤1: 创建申请 → 成功
步骤2: 风控审批 → 成功  
步骤3: 签订担保合同 → 失败 ❌
          ↓
补偿: 撤销风控审批 → 撤销申请创建 → 回滚

2.2 担保业务中的 Saga 实现

@Service
public class CreditSagaService {
    
    @Autowired
    private CreditApplyService applyService;
    @Autowired
    private RiskService riskService;
    @Autowired
    private ContractService contractService;

    public void executeCreditApply(CreditApplyRequest request) {
        SagaRuntime saga = new SagaRuntime();
        
        // 正向操作
        saga.addStep("createApply", 
            () -> applyService.create(request),
            (result) -> applyService.cancel(result.getId())  // 补偿操作
        );
        
        saga.addStep("riskReview",
            (apply) -> riskService.review(apply.getId()),
            (result) -> riskService.reject(result.getId())  // 补偿操作
        );
        
        saga.addStep("createContract",
            (riskResult) -> contractService.create(riskResult.getApplyId()),
            (result) -> contractService.cancel(result.getContractNo())  // 补偿操作
        );
        
        try {
            saga.execute();
        } catch (SagaException e) {
            saga.compensate();  // 自动回滚已执行的步骤
            log.error("信贷申请 Saga 失败,已自动补偿", e);
        }
    }
}

2.3 Saga 的优缺点

优点

  • 实现简单,每个服务只需要写正向和补偿逻辑
  • 性能好,没有两阶段锁
  • 系统可用性高,各服务独立运行

缺点

  • 必须是最终一致性,不能保证强一致性
  • 补偿操作必须幂等,且业务上可逆
  • 无法处理复杂依赖(如步骤3依赖步骤1和步骤2的结果)

2.4 担保业务适用性分析

业务场景是否适合 Saga原因
担保申请流程✅ 非常适合申请可以撤销,补偿逻辑清晰
放款操作❌ 不适合放款回滚极其复杂,涉及资金流向
合同签署⚠️ 部分适合合同可作废但需要法务流程
保证金扣划❌ 不适合资金操作需要强一致性

三、TCC 模式:最强一致性但最复杂

3.1 原理

TCC 要求每个接口实现三个方法:

阶段方法作用
Try预留资源锁定业务资源,不做实际业务操作
Confirm确认提交使用预留资源,完成实际业务
Cancel确认撤销释放预留资源
服务 A                    服务 B
  |                         |
  |-- Try: 冻结账户 1000  --->|
  |<--- Try 成功: 预留成功 ---|
  |-- Try: 扣减账户 ----->   |
  |<--- Try 成功 ----------- |
  |-- Confirm: 执行扣减 ---->|
  |<--- Confirm 成功 --------|

3.2 担保业务中的 TCC 实现

以担保费收取为例:

// Try 阶段:冻结担保费
@Component
public class GuaranteeFeeTryService implements TryExecutor {
    @Override
    public void tryAction(String bizId, String params) {
        JSONObject data = JSON.parseObject(params);
        String contractNo = data.getString("contractNo");
        BigDecimal amount = data.getBigDecimal("amount");
        
        // 冻结金额(不真正扣款)
        guaranteeMapper.freezeAmount(contractNo, amount);
        
        // 记录 TCC 事务日志
        tccLogMapper.insert(new TccLog(bizId, contractNo, "TRY", amount));
    }
}

// Confirm 阶段:真正扣款
@Component
public class GuaranteeFeeConfirmService implements ConfirmExecutor {
    @Override
    public void confirmAction(String bizId, String params) {
        // 检查是否已被 Cancel
        TccLog cancelLog = tccLogMapper.selectByBizId(bizId, "CANCEL");
        if (cancelLog != null) {
            throw new RuntimeException("已被取消,不可确认");
        }
        
        // 真正扣款
        guaranteeMapper.deductAmount(params);
        tccLogMapper.updateStatus(bizId, "CONFIRM");
    }
}

// Cancel 阶段:释放冻结
@Component
public class GuaranteeFeeCancelService implements CancelExecutor {
    @Override
    public void cancelAction(String bizId, String params) {
        // 幂等检查
        TccLog log = tccLogMapper.selectByBizId(bizId, "CONFIRM");
        if (log != null) {
            throw new RuntimeException("已确认,不可取消");
        }
        
        guaranteeMapper.unfreezeAmount(params);
        tccLogMapper.updateStatus(bizId, "CANCEL");
    }
}

3.3 TCC 的核心挑战

挑战一:空回滚

场景:Try 请求未到达服务 B,Cancel 请求先到

如果直接释放资源,后续 Try 到达时资源已被释放
解决:Cancel 检查是否存在 Try 记录,不存在则直接返回成功
public void cancelAction(String bizId) {
    // 检查是否存在 Try 记录
    TccLog tryLog = tccLogMapper.selectByBizId(bizId, "TRY");
    if (tryLog == null) {
        // 空回滚:Try 还没来,Cancel 先到,直接返回成功
        log.warn("空回滚:Try 未到达,bizId={}", bizId);
        return;
    }
    
    // 正常回滚逻辑
    releaseResources(bizId);
}

挑战二:悬挂(空悬)

场景:Cancel 先完成,然后 Try 才到达

如果 Try 正常执行,Cancel 已经释放了资源
解决:Try 检查是否存在 Cancel 记录,存在则拒绝执行
public void tryAction(String bizId) {
    // 检查是否存在 Cancel 记录
    TccLog cancelLog = tccLogMapper.selectByBizId(bizId, "CANCEL");
    if (cancelLog != null) {
        // 悬挂:Cancel 已完成,Try 不应再执行
        throw new RuntimeException("事务已取消,拒绝 Try");
    }
    
    // 正常 Try 逻辑
    reserveResources(bizId);
}

挑战三:幂等性

网络抖动导致 Confirm 或 Cancel 重试,必须保证幂等:

public void confirmAction(String bizId) {
    // 幂等检查
    int count = tccLogMapper.countByBizIdAndStatus(bizId, "CONFIRM");
    if (count > 0) {
        log.info("重复 Confirm 请求,已处理过:bizId={}", bizId);
        return;  // 幂等返回
    }
    
    // 真正执行业务
    executeBusiness(bizId);
    tccLogMapper.insert(new TccLog(bizId, "CONFIRM"));
}

3.4 TCC 的优缺点

优点

  • 一致性最强,Try 阶段就锁定了资源
  • 性能优于两阶段提交(2PC)
  • 适用于资金类等强一致性场景

缺点

  • 开发成本极高,每个接口要写 Try/Confirm/Cancel 三个方法
  • 业务侵入性强,需要改造原有业务逻辑
  • 异常处理复杂(空回滚、悬挂、幂等都需处理)

四、AT 模式:Seata 的折中方案

4.1 原理

AT 模式(Automatic Transaction)是 Seata 框架提出的模式,核心思想:

一阶段:执行 SQL 并解析 undo_log → 释放锁
二阶段提交:删除 undo_log
二阶段回滚:根据 undo_log 恢复数据
步骤        操作                        undo_log
─────────────────────────────────────────────────────
1PC-查询    SELECT * FROM account     -
1PC-更新    UPDATE account SET        记录变更前余额 2000,
            balance=1000 WHERE id=1   变更后余额 1000
1PC-提交    提交事务                   undo_log 标记为已完成

如果二阶段需要回滚:
2PC-回滚    根据 undo_log 恢复:       undo_log 被删除
            UPDATE account SET 
            balance=2000 WHERE id=1

4.2 Seata AT 模式实战

依赖引入

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>

配置文件

seata:
  enabled: true
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 192.168.1.100:8091
  data-source-proxy-mode: AT

业务代码(与普通 Spring Boot 无异):

@Service
public class GuaranteeService {
    
    @Autowired
    private GuaranteeMapper guaranteeMapper;
    @Autowired
    private CustomerMapper customerMapper;

    // 全局事务发起方
    @GlobalTransactional  // Seata 全局事务注解
    public void createGuarantee(CreateGuaranteeRequest request) {
        // 1. 创建担保记录(本服务数据库)
        guaranteeMapper.insert(request.getGuarantee());
        
        // 2. 冻结客户保证金(跨服务 RPC 调用,自动加入全局事务)
        balanceService.freezeBalance(request.getCustomerId(), 
            request.getGuaranteeAmount());
        
        // 3. 记录担保合同(另一个服务)
        contractService.createContract(request);
        
        // 任何一步失败,Seata 自动回滚所有分支事务
    }
}

被调服务(无需额外代码):

@Service
public class BalanceService {
    
    @Autowired
    private BalanceMapper balanceMapper;

    // RPC 调用,自动加入全局事务
    public void freezeBalance(Long customerId, BigDecimal amount) {
        Balance balance = balanceMapper.selectById(customerId);
        balanceMapper.freeze(customerId, amount);
        // Seata 自动拦截 SQL,生成 undo_log
    }
}

4.3 AT 模式的核心机制

undo_log 表结构

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,       -- 分支事务 ID
  `xid` varchar(100) NOT NULL,            -- 全局事务 ID
  `context` varchar(128) NOT NULL,        -- 上下文信息
  `rollback_info` longblob NOT NULL,      -- 回滚数据(序列化)
  `log_status` varchar(32) NOT NULL,      -- 状态:0=新增, 1=已提交, 2=已删除
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE=InnoDB;

提交路径(无异常):

1PC 提交 → 全局提交 → TC 通知 RM → RM 删除 undo_log(异步)

回滚路径(有异常):

1PC 回滚 → 全局回滚 → TC 通知 RM → RM 用 undo_log 恢复数据

4.4 AT 模式的优缺点

优点

  • 对业务代码侵入小,只需加 @GlobalTransactional 注解
  • 自动识别 SQL,无需手动写补偿逻辑
  • 开发体验接近本地事务

缺点

  • 需要 Seata Server 集群,运维复杂度增加
  • 一阶段需要全局锁(直到二阶段完成),影响并发性能
  • 大事务问题:长时间持有全局锁会影响吞吐量
  • 不支持跨资源的补偿(如消息队列、文件操作)

五、三种模式对比决策树

5.1 横向对比

维度SagaTCCAT
一致性强度最终一致性强一致性(阶段内)最终一致性
开发成本低(写补偿即可)高(Try/Confirm/Cancel)低(加注解即可)
性能高(无锁)中(预占资源)中(全局锁)
事务并发度中低
适用场景补偿逻辑简单资金类、强一致一般微服务
运维复杂度高(需要 Seata 集群)
框架支持自实现 / CamundaSeata / 自实现Seata
回滚确定性依赖补偿逻辑正确性确定性确定性(undo_log)

5.2 选型决策树

需要分布式事务?
  ├── 否 → 不需要分布式事务
  └── 是
      ├── 是资金操作/强一致性要求?
      │   ├── 是 → TCC(资金类场景)
      │   └── 否 ↓
      ├── 补偿逻辑是否简单可逆?
      │   ├── 是 → Saga(担保申请、审批流程)
      │   └── 否 ↓
      ├── 是否已有 Seata 基础设施?
      │   ├── 是 → AT 模式(最简单)
      │   └── 否 → 自建 AT 引擎 / 选 TCC
      └── 是否有大量并发访问?
          ├── 是 → Saga(性能最好)
          └── 否 → AT 或 TCC

5.3 担保业务的混合方案

在实际担保系统中,不同场景适用不同模式:

// 担保申请流程 → Saga
@Service
public class GuaranteeApplySaga {
    // 申请 → 审批 → 签约
    // 任一环节失败 → 回滚所有步骤
    // 补偿逻辑简单:撤销、作废
}

// 担保费扣款 → TCC
@Service
public class GuaranteeFeeTCC {
    // Try: 冻结费用
    // Confirm: 扣款
    // Cancel: 解冻
    // 资金类操作,必须强一致
}

// 客户信息更新 → AT
@GlobalTransactional
public void updateCustomerInfo(CustomerInfo info) {
    customerMapper.update(info);
    creditRecordMapper.update(info.getCustomerId());
    // 普通微服务调用,AT 足够
}

六、Seata 架构深度解析

6.1 三大角色

角色全称职责
TCTransaction Coordinator事务协调器,维护全局事务状态
TMTransaction Manager事务管理器,定义全局事务边界
RMResource Manager资源管理器,管理分支事务资源
  TM                           TC (Seata Server)
  |                               |
  |-- Begin Global TX ---------> |
  |-- Register RM -------------> |
  |                               |
  |<-- Global TX ID: 12345 ---- |
  |                               |
  |--- Branch 1 (RM1) ----------> RM1
  |<-- Branch 1 ACK -------------|
  |                               |
  |--- Branch 2 (RM2) ----------> RM2  
  |<-- Branch 2 ACK -------------|
  |                               |
  |-- Commit / Rollback ------> |
  |                               |
  |--- Commit/Branch 1 --------> RM1
  |--- Commit/Branch 2 --------> RM2

6.2 部署模式

模式事务日志存储适用场景
file本地文件开发测试
db数据库(MySQL)中小规模生产
redisRedis大规模生产,高性能
seata:
  store:
    mode: db  # 或 redis
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.1.100:3306/seata
      user: seata
      password: seata

七、总结

场景推荐方案原因
担保申请流程Saga补偿逻辑简单,最终一致性可接受
资金类操作TCC需要强一致性,防超扣
普通业务关联AT / Seata侵入小,开发快
消息通知本地消息表 / MQ 事务不需要分布式事务

核心原则

  1. 能不用就不用:优先考虑异步解耦、最终一致性
  2. 能用 Saga 不用 TCC:TCC 的开发和维护成本很高
  3. AT 是入门首选:Seata AT 模式侵入小,适合快速落地
  4. 资金类必须 TCC:一分钱都不能错,必须强一致

分布式事务没有银弹,只有适合场景的选择。理解每种模式的代价,才能在架构设计时做出正确决策。

上次编辑于:
贡献者: zhengtianqi