Seata 分布式事务:从入门到精通的完整指南

jxq
2
2026-06-29

Seata 分布式事务:从入门到精通的完整指南

本文将带你深入理解 Seata 分布式事务解决方案,从基础概念到生产实践,循序渐进地掌握分布式事务的核心原理与实战技巧。

适用版本:Seata 1.7+ / 2.x | Spring Boot 2.7+ / 3.x | Spring Cloud Alibaba 2021+


第一部分:基础入门

1.1 Seata 是什么?

在单体架构时代,事务管理很简单——Spring 的 @Transactional 注解就能搞定。数据库的 ACID 特性让一切看起来都很美好。

但在微服务架构下,问题来了:

订单服务 → 扣减库存 → 扣减余额
   ↓          ↓          ↓
 订单DB     库存DB      账户DB

一个业务操作跨越了三个数据库,如何保证要么全部成功,要么全部失败?这就是分布式事务要解决的问题。

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。它前身是 Fescar(Fast & Easy Commit And Rollback),2019 年更名为 Seata。

核心价值

  • ✅ 对业务零侵入(AT 模式下)
  • ✅ 支持多种事务模式(AT、TCC、SAGA、XA)
  • ✅ 高性能(单机支持万级 TPS)
  • ✅ 生态丰富(支持 Spring Cloud、Dubbo 等)

1.2 分布式事务的痛点

在没有分布式事务框架之前,常见的解决方案有:

方案 优点 缺点
2PC(两阶段提交) 强一致性 性能差、阻塞、单点故障
TCC(Try-Confirm-Cancel) 性能好 代码侵入性强,需要写补偿逻辑
本地消息表 简单可靠 不实时,需要定时任务轮询
事务消息 解耦 只能保证最终一致性

痛点总结

  1. 实现复杂:需要手动处理各种异常场景
  2. 代码侵入:业务代码与事务代码耦合
  3. 难以维护:分布式事务的边界不清晰
  4. 缺乏工具:没有统一的事务协调器

Seata 的出现,就是为了解决这些痛点。


1.3 Seata 的核心概念

Seata 的架构非常清晰,包含三个核心角色:

graph LR
    TM[TM 事务管理器<br/>Transaction Manager] -->|开启/提交/回滚| TC[TC 事务协调器<br/>Transaction Coordinator]
    RM1[RM 资源管理器<br/>Resource Manager] -->|注册分支事务| TC
    RM2[RM 资源管理器<br/>Resource Manager] -->|注册分支事务| TC
    TC -->|协调| RM1
    TC -->|协调| RM2
    
    style TC fill:#ff9999,stroke:#333,stroke-width:3px
    style TM fill:#99ff99,stroke:#333
    style RM1 fill:#9999ff,stroke:#333
    style RM2 fill:#9999ff,stroke:#333

角色职责

TC (Transaction Coordinator) - 事务协调器

  • 维护全局事务的状态
  • 协调全局事务的提交或回滚
  • 通常是一个独立的服务(Seata Server)

TM (Transaction Manager) - 事务管理器

  • 开启全局事务
  • 提交或回滚全局事务
  • 通常在业务发起方(微服务中的调用方)

RM (Resource Manager) - 资源管理器

  • 管理分支事务
  • 向 TC 注册分支事务
  • 汇报分支事务状态
  • 通常在业务参与方(微服务中的被调用方)

一个典型的事务流程

sequenceDiagram
    participant TM as TM(订单服务)
    participant TC as TC(Seata Server)
    participant RM1 as RM(库存服务)
    participant RM2 as RM(账户服务)
    
    TM->>TC: 1. 开启全局事务
    TC-->>TM: XID (全局事务ID)
    
    TM->>RM1: 2. 调用库存服务(XID传播)
    RM1->>TC: 3. 注册分支事务
    TC-->>RM1: 分支事务ID
    RM1->>RM1: 执行本地事务
    RM1->>TC: 4. 汇报分支状态
    
    TM->>RM2: 5. 调用账户服务(XID传播)
    RM2->>TC: 6. 注册分支事务
    TC-->>RM2: 分支事务ID
    RM2->>RM2: 执行本地事务
    RM2->>TC: 7. 汇报分支状态
    
    alt 所有分支成功
        TM->>TC: 8. 提交全局事务
        TC->>RM1: 提交分支事务
        TC->>RM2: 提交分支事务
    else 有分支失败
        TM->>TC: 8. 回滚全局事务
        TC->>RM1: 回滚分支事务
        TC->>RM2: 回滚分支事务
    end

1.4 四种事务模式对比

Seata 提供了四种事务模式,每种都有其适用场景:

AT 模式(Auto Transaction)

核心思想:基于支持本地 ACID 事务的关系型数据库,自动记录前后镜像,实现无侵入的分布式事务。

// 业务代码完全无感知
@GlobalTransactional
public void placeOrder(Order order) {
    orderMapper.insert(order);          // 本地事务
    storageService.deduct(order.getItemId(), order.getCount());  // 远程调用
    accountService.debit(order.getUserId(), order.getAmount());  // 远程调用
}

优点

  • 零侵入,业务代码无需改动
  • 一阶段提交,性能好
  • 学习成本低

缺点

  • 只支持关系型数据库
  • 存在脏写风险(需要全局锁)

适用场景:大多数基于关系型数据库的微服务系统


TCC 模式(Try-Confirm-Cancel)

核心思想:将业务逻辑拆分为三个阶段——Try(预留资源)、Confirm(确认提交)、Cancel(取消回滚)。

public interface StorageTccService {
    
    // Try: 预留库存
    boolean tryDeduct(String itemId, int count);
    
    // Confirm: 确认扣减
    boolean confirmDeduct(String itemId, int count);
    
    // Cancel: 释放预留的库存
    boolean cancelDeduct(String itemId, int count);
}

实现示例

@Service
public class StorageTccServiceImpl implements StorageTccService {
    
    @Autowired
    private StorageMapper storageMapper;
    
    @Override
    public boolean tryDeduct(String itemId, int count) {
        // 预留库存:冻结库存
        int rows = storageMapper.freezeStock(itemId, count);
        return rows > 0;
    }
    
    @Override
    public boolean confirmDeduct(String itemId, int count) {
        // 确认扣减:减少冻结库存
        int rows = storageMapper.deductFrozenStock(itemId, count);
        return rows > 0;
    }
    
    @Override
    public boolean cancelDeduct(String itemId, int count) {
        // 取消:释放冻结库存
        int rows = storageMapper.releaseFrozenStock(itemId, count);
        return rows > 0;
    }
}

优点

  • 性能最优(锁粒度最小)
  • 支持非关系型数据库

缺点

  • 代码侵入性强
  • 需要实现三个接口
  • 业务逻辑复杂

适用场景:对性能要求高、数据库类型多样、业务允许幂等


SAGA 模式

核心思想:将长事务拆分为多个本地短事务,每个短事务都有对应的补偿事务。如果某个环节失败,则反向执行补偿事务。

graph LR
    T1[事务T1<br/>创建订单] --> T2[事务T2<br/>扣减库存]
    T2 --> T3[事务T3<br/>扣减余额]
    T3 --> T4[事务T4<br/>发送通知]
    
    T1 -.-> C1[补偿C1<br/>取消订单]
    T2 -.-> C2[补偿C2<br/>恢复库存]
    T3 -.-> C3[补偿C3<br/>恢复余额]
    T4 -.-> C4[补偿C4<br/>撤回通知]
    
    style T1 fill:#99ff99
    style T2 fill:#99ff99
    style T3 fill:#99ff99
    style T4 fill:#99ff99
    style C1 fill:#ff9999
    style C2 fill:#ff9999
    style C3 fill:#ff9999
    style C4 fill:#ff9999

状态机定义(JSON):

{
  "Name": "placeOrder",
  "Comment": "下单流程",
  "StartState": "CreateOrder",
  "States": [
    {
      "Name": "CreateOrder",
      "Type": "ServiceTask",
      "ServiceName": "orderService",
      "ServiceMethod": "create",
      "Next": "DeductStock"
    },
    {
      "Name": "DeductStock",
      "Type": "ServiceTask",
      "ServiceName": "storageService",
      "ServiceMethod": "deduct",
      "Next": "DeductBalance",
      "CompensateState": "CompensateStock"
    },
    {
      "Name": "DeductBalance",
      "Type": "ServiceTask",
      "ServiceName": "accountService",
      "ServiceMethod": "debit",
      "CompensateState": "CompensateBalance"
    }
  ]
}

优点

  • 适合长事务
  • 可视化流程编排
  • 支持异步

缺点

  • 缺乏隔离性(中间状态可见)
  • 补偿逻辑复杂

适用场景:业务流程长、跨系统、需要人工干预的场景


XA 模式

核心思想:基于数据库的 XA 协议实现的两阶段提交。

@GlobalTransactional
public void placeOrder(Order order) {
    // XA 协议:数据库层面支持
    orderMapper.insert(order);          // XA START → XA END → XA PREPARE
    storageService.deduct(itemId, count);  // 同上
    accountService.debit(userId, amount);  // 同上
    // 全局提交时:XA COMMIT
}

优点

  • 强一致性
  • 标准协议,兼容性好

缺点

  • 性能差(两阶段阻塞)
  • 数据库支持有限(MySQL 5.7+、PostgreSQL 等)

适用场景:对一致性要求极高、并发量不大的系统


四种模式对比表

特性 AT TCC SAGA XA
一致性 最终一致 最终一致 最终一致 强一致
隔离性 全局锁 业务锁 无隔离 全局锁
性能 最高
代码侵入
数据库支持 关系型 任意 任意 XA 支持
适用场景 常规业务 高性能/非关系型 长事务 强一致性

第二部分:快速上手

2.1 Spring Boot 集成 Seata

环境准备

组件 版本
Spring Boot 2.7.18
Spring Cloud Alibaba 2021.0.8.0
Seata 1.7.1
MySQL 8.0+

第一步:下载 Seata Server

# 下载 Seata Server(推荐 1.7.x 或 2.x)
wget https://github.com/seata/seata/releases/download/v1.7.1/seata-server-1.7.1.zip

# 解压
unzip seata-server-1.7.1.zip
cd seata-server-1.7.1

# 启动(默认端口 8091)
sh bin/seata-server.sh

第二步:创建项目

Maven 依赖

<!-- 父工程 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<!-- Seata 依赖 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.1</version>
</dependency>

<!-- 如果使用 Spring Cloud Alibaba -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2021.0.8.0</version>
</dependency>

第三步:配置文件

application.yml

seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: file
  registry:
    type: file
  
  # AT 模式配置
  data-source-proxy-mode: AT
  
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root123

配置说明

配置项 说明
application-id 应用唯一标识
tx-service-group 事务分组(逻辑分组)
vgroup-mapping 事务分组到集群的映射
grouplist Seata Server 地址
data-source-proxy-mode 事务模式(AT、XA)

2.2 AT 模式实战

场景说明

实现一个经典的下单扣库存扣余额流程:

订单服务 → 库存服务 → 账户服务

第一步:准备数据库

每个微服务都需要一个 undo_log 表(AT 模式必需):

-- 每个数据库都需要这张表
CREATE TABLE `undo_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) 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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 订单表
CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `item_id` bigint NOT NULL,
  `count` int NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  `status` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 库存表
CREATE TABLE `t_storage` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `item_id` bigint NOT NULL,
  `count` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 账户表
CREATE TABLE `t_account` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `balance` decimal(10,2) NOT NULL DEFAULT '0.00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第二步:编写业务代码

订单服务(TM - 事务发起方)

@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private StorageClient storageClient;
    
    @Autowired
    private AccountClient accountClient;
    
    /**
     * 下单:开启全局事务
     */
    @GlobalTransactional(name = "place-order", rollbackFor = Exception.class)
    @Override
    public void placeOrder(Order order) {
        // 1. 创建订单(本地事务)
        orderMapper.insert(order);
        
        // 2. 扣减库存(远程调用)
        storageClient.deduct(order.getItemId(), order.getCount());
        
        // 3. 扣减余额(远程调用)
        accountClient.debit(order.getUserId(), order.getAmount());
        
        // 4. 模拟异常
        if (order.getAmount().compareTo(new BigDecimal("1000")) > 0) {
            throw new RuntimeException("金额超过限额,触发回滚");
        }
    }
}

库存服务(RM - 事务参与方)

@Service
public class StorageServiceImpl implements StorageService {
    
    @Autowired
    private StorageMapper storageMapper;
    
    @Override
    public void deduct(String itemId, int count) {
        // 本地事务,自动注册为分支事务
        int rows = storageMapper.deduct(itemId, count);
        if (rows == 0) {
            throw new RuntimeException("库存不足");
        }
    }
}

账户服务(RM - 事务参与方)

@Service
public class AccountServiceImpl implements AccountService {
    
    @Autowired
    private AccountMapper accountMapper;
    
    @Override
    public void debit(String userId, BigDecimal amount) {
        // 本地事务,自动注册为分支事务
        int rows = accountMapper.debit(userId, amount);
        if (rows == 0) {
            throw new RuntimeException("余额不足");
        }
    }
}

第三步:配置数据源代理

Seata 需要代理数据源才能拦截 SQL:

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }
    
    /**
     * Seata 数据源代理
     * AT 模式:DataSourceProxy
     * XA 模式:DataSourceProxyXA
     */
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
}

注意:Spring Boot 2.x + MyBatis 需要手动配置 SqlSessionFactory:

@Configuration
public class MyBatisConfig {
    
    @Autowired
    private DataSourceProxy dataSourceProxy;
    
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSourceProxy);  // 使用代理数据源
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources("classpath:mapper/*.xml"));
        return factory.getObject();
    }
}

第四步:测试验证

@SpringBootTest
public class OrderServiceTest {
    
    @Autowired
    private OrderService orderService;
    
    @Test
    public void testPlaceOrder_Success() {
        Order order = new Order();
        order.setUserId(1L);
        order.setItemId(1L);
        order.setCount(2);
        order.setAmount(new BigDecimal("200"));
        
        orderService.placeOrder(order);  // 正常提交
    }
    
    @Test
    public void testPlaceOrder_Rollback() {
        Order order = new Order();
        order.setUserId(1L);
        order.setItemId(1L);
        order.setCount(2);
        order.setAmount(new BigDecimal("1500"));  // 超过限额
        
        orderService.placeOrder(order);  // 触发回滚
    }
}

2.3 配置详解

Seata 有两个核心配置文件:file.confregistry.conf

file.conf(事务配置)

## 事务配置
service {
  # 事务分组映射
  vgroup_mapping.my_tx_group = "default"
  
  # Seata Server 地址(仅 file 模式需要)
  grouplist.default = "127.0.0.1:8091"
  
  # 全局事务开关
  enableDegrade = false
  disableGlobalTransaction = false
}

## 客户端配置
client {
  ## 异步提交缓冲区大小
  async.commit.buffer.limit = 10000
  
  ## 报告重试次数
  report.retry.count = 5
  
  ## 是否上报成功
  report.success.enable = false
  
  ## 是否上报成功状态
  saga.retry.persist.mode.update = false
  
  ## 全局事务超时时间(毫秒)
  default.global.transaction.timeout = 60000
  
  ## 是否解除数据源代理支持
  undo.data.validation = true
  
  ## undo_log 表名
  undo.log.table = undo_log
  
  ## 只读事务是否传播全局事务
  rm.async.commit.buffer.limit = 10000
  rm.report.retry.count = 5
  rm.table.meta.check.enable = true
  rm.report.success.enable = false
  
  ## TM 配置
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
  
  ## undo 配置
  undo.only.care.update.columns = true
}

## 存储模式(Server 端)
store {
  mode = "file"  # file、db、redis
  
  file {
    dir = "sessionStore"
    max.branch.session.size = 16384
    max.global.session.size = 512
    file.write.buffer.size = 16384
  }
  
  db {
    datasource = "druid"
    dbType = "mysql"
    url = "jdbc:mysql://localhost:3306/seata"
    user = "root"
    password = "root"
  }
}

registry.conf(注册中心配置)

## 注册中心配置
registry {
  type = "file"  # file、nacos、eureka、redis、zk、consul、etcd3、sofa
  
  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  
  file {
    name = "file.conf"
  }
}

## 配置中心
config {
  type = "file"  # file、nacos、apollo、zk、consul、etcd3
  
  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  
  file {
    name = "file.conf"
  }
}

2.4 数据库脚本准备

Seata Server 需要的表

如果是生产环境,建议使用数据库存储事务日志:

-- 全局事务表
CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint DEFAULT NULL,
  `status` tinyint NOT NULL,
  `application_id` varchar(32) DEFAULT NULL,
  `transaction_service_group` varchar(32) DEFAULT NULL,
  `transaction_name` varchar(128) DEFAULT NULL,
  `timeout` int DEFAULT NULL,
  `begin_time` bigint DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_status_gmt_modified` (`status`,`gmt_modified`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 分支事务表
CREATE TABLE `branch_table` (
  `branch_id` bigint NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint DEFAULT NULL,
  `resource_group_id` varchar(32) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 全局锁表
CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,
  `xid` varchar(128) DEFAULT NULL,
  `transaction_id` bigint DEFAULT NULL,
  `branch_id` bigint NOT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `table_name` varchar(32) DEFAULT NULL,
  `pk` varchar(36) DEFAULT NULL,
  `status` tinyint NOT NULL DEFAULT '0',
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`),
  KEY `idx_status` (`status`),
  KEY `idx_branch_id` (`branch_id`),
  KEY `idx_xid_and_branch_id` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第三部分:进阶实战

3.1 TCC 模式详解与实战

核心概念

TCC 模式将业务逻辑分为三个阶段:

graph LR
    A[Try 尝试] --> B{成功?}
    B -->|是| C[Confirm 确认]
    B -->|否| D[Cancel 取消]
    
    style A fill:#ffff99
    style C fill:#99ff99
    style D fill:#ff9999
  • Try:预留资源(如:冻结库存)
  • Confirm:确认提交(如:扣减冻结库存)
  • Cancel:取消回滚(如:释放冻结库存)

实战案例:库存扣减

第一步:设计数据库表

-- 库存表(需要增加冻结字段)
CREATE TABLE `t_storage` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `item_id` bigint NOT NULL,
  `count` int NOT NULL DEFAULT '0',
  `frozen_count` int NOT NULL DEFAULT '0',  -- 冻结数量
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第二步:定义 TCC 接口

@LocalTCC
public interface StorageTccService {
    
    /**
     * Try: 预留库存
     * @BusinessActionContextParameter 注解的参数会存入上下文
     */
    @TwoPhaseBusinessAction(
        name = "storageTcc",
        commitMethod = "commit",
        rollbackMethod = "rollback"
    )
    boolean prepareDeduct(
        @BusinessActionContextParameter(paramName = "itemId") String itemId,
        @BusinessActionContextParameter(paramName = "count") int count
    );
    
    /**
     * Confirm: 提交扣减
     */
    boolean commit(BusinessActionContext context);
    
    /**
     * Cancel: 回滚
     */
    boolean rollback(BusinessActionContext context);
}

第三步:实现 TCC 接口

@Service
@Slf4j
public class StorageTccServiceImpl implements StorageTccService {
    
    @Autowired
    private StorageMapper storageMapper;
    
    /**
     * Try 阶段:冻结库存
     */
    @Override
    public boolean prepareDeduct(String itemId, int count) {
        log.info("Try: 预留库存, itemId={}, count={}", itemId, count);
        
        // 检查库存是否充足
        Storage storage = storageMapper.selectByItemId(itemId);
        if (storage.getCount() < count) {
            throw new RuntimeException("库存不足");
        }
        
        // 冻结库存
        int rows = storageMapper.freezeStock(itemId, count);
        return rows > 0;
    }
    
    /**
     * Confirm 阶段:真正扣减
     */
    @Override
    public boolean commit(BusinessActionContext context) {
        String itemId = context.getActionContext("itemId", String.class);
        Integer count = context.getActionContext("count", Integer.class);
        
        log.info("Confirm: 扣减库存, itemId={}, count={}", itemId, count);
        
        // 减少冻结库存
        int rows = storageMapper.deductFrozenStock(itemId, count);
        return rows > 0;
    }
    
    /**
     * Cancel 阶段:释放冻结库存
     */
    @Override
    public boolean rollback(BusinessActionContext context) {
        String itemId = context.getActionContext("itemId", String.class);
        Integer count = context.getActionContext("count", Integer.class);
        
        log.info("Cancel: 释放库存, itemId={}, count={}", itemId, count);
        
        // 释放冻结库存
        int rows = storageMapper.releaseFrozenStock(itemId, count);
        return rows > 0;
    }
}

Mapper SQL

<mapper namespace="com.example.mapper.StorageMapper">
    
    <!-- 冻结库存 -->
    <update id="freezeStock">
        UPDATE t_storage
        SET count = count - #{count},
            frozen_count = frozen_count + #{count}
        WHERE item_id = #{itemId} AND count >= #{count}
    </update>
    
    <!-- 扣减冻结库存 -->
    <update id="deductFrozenStock">
        UPDATE t_storage
        SET frozen_count = frozen_count - #{count}
        WHERE item_id = #{itemId} AND frozen_count >= #{count}
    </update>
    
    <!-- 释放冻结库存 -->
    <update id="releaseFrozenStock">
        UPDATE t_storage
        SET count = count + #{count},
            frozen_count = frozen_count - #{count}
        WHERE item_id = #{itemId} AND frozen_count >= #{count}
    </update>
    
</mapper>

第四步:调用 TCC 服务

@Service
public class OrderServiceImpl {
    
    @Autowired
    private StorageTccService storageTccService;
    
    @GlobalTransactional
    public void placeOrder(Order order) {
        // 创建订单
        orderMapper.insert(order);
        
        // TCC 方式扣减库存
        boolean success = storageTccService.prepareDeduct(
            order.getItemId(), 
            order.getCount()
        );
        
        if (!success) {
            throw new RuntimeException("预留库存失败");
        }
        
        // 其他业务逻辑...
    }
}

TCC 关键问题

1. 空回滚

问题:Try 未执行成功,但 Cancel 被调用了。

解决方案:在 Cancel 方法中判断 Try 是否执行过。

@Override
public boolean rollback(BusinessActionContext context) {
    String itemId = context.getActionContext("itemId", String.class);
    Integer count = context.getActionContext("count", Integer.class);
    
    // 检查是否有冻结记录(判断 Try 是否执行过)
    Storage storage = storageMapper.selectByItemId(itemId);
    if (storage.getFrozenCount() < count) {
        log.warn("空回滚: itemId={}, 没有冻结记录", itemId);
        return true;  // 直接返回成功
    }
    
    // 正常回滚逻辑
    return storageMapper.releaseFrozenStock(itemId, count) > 0;
}

2. 悬挂

问题:Cancel 比 Try 先执行,导致 Try 预留的资源无法释放。

解决方案:记录事务状态,防止悬挂。

// 增加事务状态表
CREATE TABLE `tcc_transaction` (
  `xid` varchar(128) NOT NULL,
  `branch_id` bigint NOT NULL,
  `status` varchar(10) DEFAULT NULL,  -- TRY、CONFIRM、CANCEL
  PRIMARY KEY (`xid`, `branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

@Override
public boolean prepareDeduct(String itemId, int count) {
    String xid = RootContext.getXID();
    String branchId = String.valueOf(RootContext.getBranchId());
    
    // 检查是否已经 Cancel 过
    TccTransaction tx = tccMapper.selectByXidAndBranchId(xid, branchId);
    if (tx != null && "CANCEL".equals(tx.getStatus())) {
        log.warn("悬挂: xid={}, branchId={}, 已经 Cancel 过", xid, branchId);
        return false;
    }
    
    // 记录 Try 状态
    tccMapper.insert(xid, branchId, "TRY");
    
    // 正常 Try 逻辑
    // ...
}

@Override
public boolean rollback(BusinessActionContext context) {
    String xid = context.getXid();
    String branchId = String.valueOf(context.getBranchId());
    
    // 记录 Cancel 状态
    tccMapper.insert(xid, branchId, "CANCEL");
    
    // 正常 Cancel 逻辑
    // ...
}

3. 幂等性

问题:Confirm 或 Cancel 可能被重复调用。

解决方案:使用事务状态判断。

@Override
public boolean commit(BusinessActionContext context) {
    String xid = context.getXid();
    String branchId = String.valueOf(context.getBranchId());
    
    // 检查是否已经 Confirm 过
    TccTransaction tx = tccMapper.selectByXidAndBranchId(xid, branchId);
    if (tx != null && "CONFIRM".equals(tx.getStatus())) {
        log.info("幂等: xid={}, branchId={}, 已经 Confirm 过", xid, branchId);
        return true;
    }
    
    // 正常 Confirm 逻辑
    // ...
    
    // 更新状态
    tccMapper.updateStatus(xid, branchId, "CONFIRM");
    return true;
}

3.2 SAGA 模式适用场景

适用场景

  1. 长事务:业务流程长,不适合 2PC
  2. 跨系统:涉及第三方系统,无法保证一致性
  3. 人工干预:某些环节需要人工审核
  4. 异步流程:需要异步执行的业务

实战示例

状态机定义(JSON):

{
  "Name": "orderProcess",
  "Comment": "订单处理流程",
  "StartState": "CreateOrder",
  "Version": "1.0.0",
  "States": [
    {
      "Name": "CreateOrder",
      "Type": "ServiceTask",
      "Comment": "创建订单",
      "ServiceName": "orderService",
      "ServiceMethod": "create",
      "Next": "DeductStock",
      "Input": [
        "$.[0].orderId",
        "$.[0].userId",
        "$.[0].amount"
      ],
      "Output": {
        "createOrderResult": "$"
      }
    },
    {
      "Name": "DeductStock",
      "Type": "ServiceTask",
      "Comment": "扣减库存",
      "ServiceName": "stockService",
      "ServiceMethod": "deduct",
      "Next": "DeductBalance",
      "CompensateState": "CompensateStock",
      "Input": [
        "$.[0].itemId",
        "$.[0].count"
      ]
    },
    {
      "Name": "DeductBalance",
      "Type": "ServiceTask",
      "Comment": "扣减余额",
      "ServiceName": "balanceService",
      "ServiceMethod": "deduct",
      "CompensateState": "CompensateBalance",
      "Input": [
        "$.[0].userId",
        "$.[0].amount"
      ]
    },
    {
      "Name": "CompensateStock",
      "Type": "CompensateSubMachine",
      "Comment": "补偿库存",
      "ServiceName": "stockService",
      "ServiceMethod": "compensate"
    },
    {
      "Name": "CompensateBalance",
      "Type": "CompensateSubMachine",
      "Comment": "补偿余额",
      "ServiceName": "balanceService",
      "ServiceMethod": "compensate"
    }
  ]
}

Spring Boot 配置

@Configuration
public class SagaConfig {
    
    @Bean
    public StateMachineEngine stateMachineEngine(DataSource dataSource) {
        DbStateMachineConfig config = new DbStateMachineConfig();
        config.setDataSource(dataSource);
        config.setApplicationId("order-service");
        config.setTxServiceGroup("my_tx_group");
        config.setTablePrefix("seata_");
        
        return new ProcessCtrlStateMachineEngine(config);
    }
}

3.3 多数据源事务处理

场景说明

一个微服务中访问多个数据库,需要保证事务一致性。

实现方式

@Configuration
public class MultiDataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return new DruidDataSource();
    }
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return new DruidDataSource();
    }
    
    /**
     * 为每个数据源创建代理
     */
    @Bean
    public DataSourceProxy primaryDataSourceProxy(DataSource primaryDataSource) {
        return new DataSourceProxy(primaryDataSource);
    }
    
    @Bean
    public DataSourceProxy secondaryDataSourceProxy(DataSource secondaryDataSource) {
        return new DataSourceProxy(secondaryDataSource);
    }
    
    /**
     * 多数据源路由
     */
    @Bean
    @Primary
    public DataSource routingDataSource(
            DataSourceProxy primaryDataSourceProxy,
            DataSourceProxy secondaryDataSourceProxy) {
        
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("primary", primaryDataSourceProxy);
        targetDataSources.put("secondary", secondaryDataSourceProxy);
        
        RoutingDataSource routing = new RoutingDataSource();
        routing.setDefaultTargetDataSource(primaryDataSourceProxy);
        routing.setTargetDataSources(targetDataSources);
        
        return routing;
    }
}

3.4 事务分组与高可用部署

事务分组

概念:事务分组是逻辑上的分组,映射到实际的 Seata Server 集群。

seata:
  tx-service-group: my_tx_group  # 逻辑分组
  service:
    vgroup-mapping:
      my_tx_group: cluster1      # 映射到集群 cluster1

优势

  • 灵活切换 Seata Server 集群
  • 支持多租户隔离
  • 方便灰度发布

高可用部署

graph TB
    Client1[微服务A] --> LB[Nginx/LB]
    Client2[微服务B] --> LB
    Client3[微服务C] --> LB
    
    LB --> Server1[Seata Server 1]
    LB --> Server2[Seata Server 2]
    LB --> Server3[Seata Server 3]
    
    Server1 --> DB[(MySQL 集群)]
    Server2 --> DB
    Server3 --> DB
    
    style LB fill:#ff9999
    style Server1 fill:#99ff99
    style Server2 fill:#99ff99
    style Server3 fill:#99ff99

部署要点

  1. 注册中心:使用 Nacos/ZooKeeper 管理 Seata Server
  2. 数据库:使用 MySQL 集群存储事务日志
  3. 负载均衡:Nginx 或客户端负载均衡

配置示例(Nacos):

seata:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: nacos-cluster:8848
      namespace: ""
      group: SEATA_GROUP
      cluster: default
  
  config:
    type: nacos
    nacos:
      server-addr: nacos-cluster:8848
      namespace: ""
      group: SEATA_GROUP

第四部分:原理深入

4.1 AT 模式的实现原理

AT 模式是 Seata 的核心创新,实现了无侵入的分布式事务

核心机制:前后镜像

sequenceDiagram
    participant App as 应用程序
    participant Proxy as DataSourceProxy
    participant DB as 数据库
    participant Undo as undo_log
    
    App->>Proxy: UPDATE t_stock SET count=90 WHERE id=1
    Proxy->>DB: SELECT * FROM t_stock WHERE id=1 (前镜像)
    DB-->>Proxy: {id:1, count:100}
    
    Proxy->>DB: UPDATE t_stock SET count=90 WHERE id=1
    Proxy->>DB: SELECT * FROM t_stock WHERE id=1 (后镜像)
    DB-->>Proxy: {id:1, count:90}
    
    Proxy->>Undo: INSERT INTO undo_log (前后镜像)
    
    Note over Proxy,Undo: 全局提交时删除 undo_log<br/>全局回滚时根据 undo_log 回滚

详细流程

一阶段(业务 SQL 执行)

  1. 解析 SQL,得到表名、条件
  2. 查询前镜像(SELECT * FROM table WHERE condition)
  3. 执行业务 SQL
  4. 查询后镜像(SELECT * FROM table WHERE condition)
  5. 生成回滚 SQL
  6. 插入 undo_log

undo_log 结构

{
  "branchId": 123456,
  "xid": "192.168.1.1:8091:12345",
  "beforeImage": {
    "tableName": "t_stock",
    "rows": [
      {
        "fields": [
          {"name": "id", "type": "BIGINT", "value": 1},
          {"name": "count", "type": "INT", "value": 100}
        ]
      }
    ]
  },
  "afterImage": {
    "tableName": "t_stock",
    "rows": [
      {
        "fields": [
          {"name": "id", "type": "BIGINT", "value": 1},
          {"name": "count", "type": "INT", "value": 90}
        ]
      }
    ]
  }
}

二阶段(提交/回滚)

提交

  • 异步删除 undo_log
  • 释放全局锁

回滚

  1. 根据 undo_log 生成反向 SQL
  2. 执行反向 SQL(恢复数据)
  3. 删除 undo_log

4.2 全局锁机制

为什么需要全局锁?

在 AT 模式下,一阶段本地事务已提交,数据可能被其他事务修改。全局锁确保隔离性

全局锁原理

sequenceDiagram
    participant Tx1 as 全局事务1
    participant TC as TC
    participant DB as 数据库
    participant Tx2 as 全局事务2
    
    Tx1->>DB: UPDATE t_stock SET count=90 WHERE id=1
    Tx1->>TC: 申请全局锁 (id=1)
    TC-->>Tx1: 获取成功
    Tx1->>DB: 提交本地事务
    
    Tx2->>DB: UPDATE t_stock SET count=80 WHERE id=1
    Tx2->>TC: 申请全局锁 (id=1)
    TC-->>Tx2: 锁冲突,等待
    
    Tx1->>TC: 全局提交,释放全局锁
    Tx2->>TC: 再次申请全局锁
    TC-->>Tx2: 获取成功
    Tx2->>DB: 提交本地事务

全局锁表结构

CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,  -- lock_key = table_name + pk
  `xid` varchar(128) NOT NULL,
  `branch_id` bigint NOT NULL,
  PRIMARY KEY (`row_key`)
);

全局锁获取流程

// 伪代码
public boolean tryLock(String lockKey, String xid) {
    // 1. 查询是否已被锁定
    Lock lock = lockMapper.selectByLockKey(lockKey);
    
    if (lock == null) {
        // 2. 未锁定,插入锁记录
        return lockMapper.insert(lockKey, xid) > 0;
    } else if (lock.getXid().equals(xid)) {
        // 3. 已被自己锁定
        return true;
    } else {
        // 4. 被其他事务锁定
        return false;
    }
}

4.3 事务传播与隔离级别

XID 传播

全局事务 ID(XID)需要在微服务间传播:

graph LR
    A[订单服务<br/>XID: 123] -->|HTTP Header| B[库存服务<br/>XID: 123]
    B -->|HTTP Header| C[账户服务<br/>XID: 123]
    
    style A fill:#99ff99
    style B fill:#99ff99
    style C fill:#99ff99

传播方式

  1. HTTP:通过 Header 传递
  2. RPC:通过 Attachment 传递(Dubbo)
  3. MQ:通过 Message Property 传递

Spring Cloud OpenFeign 示例

@Configuration
public class FeignConfig {
    
    @Bean
    public RequestInterceptor seataInterceptor() {
        return template -> {
            String xid = RootContext.getXID();
            if (xid != null) {
                template.header("TX_XID", xid);
            }
        };
    }
}

// 接收方
@Component
public class SeataFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        HttpServletRequest req = (HttpServletRequest) request;
        String xid = req.getHeader("TX_XID");
        
        if (xid != null) {
            RootContext.bind(xid);
        }
        
        try {
            chain.doFilter(request, response);
        } finally {
            RootContext.unbind();
        }
    }
}

隔离级别

Seata AT 模式的隔离级别:

隔离级别 说明
读未提交 默认,可能读到中间状态
读已提交 需要配置全局锁

配置读已提交

@Service
public class OrderService {
    
    @GlobalTransactional(isolation = Isolation.READ_COMMITTED)
    public void placeOrder(Order order) {
        // 业务逻辑
    }
}

4.4 事务日志与恢复机制

事务日志存储

Seata Server 使用事务日志记录全局事务状态:

erDiagram
    GLOBAL_TABLE {
        varchar xid PK
        bigint transaction_id
        tinyint status
        varchar application_id
        datetime gmt_create
    }
    
    BRANCH_TABLE {
        bigint branch_id PK
        varchar xid FK
        varchar resource_id
        varchar branch_type
        tinyint status
    }
    
    GLOBAL_TABLE ||--o{ BRANCH_TABLE : contains

事务状态流转

stateDiagram-v2
    [*] --> Begin: 开启全局事务
    Begin --> Committing: 所有分支成功
    Begin --> Rollbacking: 有分支失败
    Committing --> Committed: 提交成功
    Rollbacking --> Rollbacked: 回滚成功
    Committed --> [*]
    Rollbacked --> [*]

恢复机制

场景 1:TM 崩溃

  • TC 超时后自动回滚

场景 2:RM 崩溃

  • TC 记录分支事务状态
  • RM 重启后根据 undo_log 回滚

场景 3:TC 崩溃

  • 事务日志持久化到数据库
  • TC 重启后恢复事务状态

第五部分:生产实践

5.1 性能优化建议

1. 数据库优化

-- undo_log 表优化
CREATE TABLE `undo_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) 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`),
  KEY `idx_log_created` (`log_created`)  -- 定期清理用
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 定期清理已提交的 undo_log
DELETE FROM undo_log 
WHERE log_created < DATE_SUB(NOW(), INTERVAL 7 DAY) 
AND log_status = 0;

2. 连接池优化

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 5
      min-idle: 10
      max-active: 50
      max-wait: 60000
      
      # 关键配置:关闭自动提交
      default-auto-commit: false

3. Seata Server 优化

## server 配置
server {
  ## 最大并发全局事务
  max.commit.retry.timeout = 100
  max.rollback.retry.timeout = 100
  
  ## 重试次数
  retry.commit.times = 3
  retry.rollback.times = 3
}

## 存储(建议使用 DB 或 Redis)
store {
  mode = "db"
  
  db {
    datasource = "druid"
    dbType = "mysql"
    url = "jdbc:mysql://mysql-cluster:3306/seata"
    user = "root"
    password = "root"
    
    ## 连接池配置
    min-conn = 10
    max-conn = 100
  }
}

4. 异步提交

// 开启异步提交(默认)
@GlobalTransactional(rollbackFor = Exception.class)
public void placeOrder(Order order) {
    // 业务逻辑
}

5.2 常见问题与解决方案

问题 1:全局锁获取失败

错误信息

io.seata.rm.datasource.exec.LockConflictException: get global lock fail

原因

  • 多个全局事务同时修改同一行数据
  • 全局锁超时

解决方案

// 增加重试
@GlobalTransactional(
    rollbackFor = Exception.class,
    lockRetryInternal = 10,  // 重试间隔(毫秒)
    lockRetryTimes = 30      // 重试次数
)
public void placeOrder(Order order) {
    // 业务逻辑
}

问题 2:undo_log 表不存在

错误信息

Table 'order_db.undo_log' doesn't exist

解决方案:每个数据库都需要创建 undo_log 表(见上文)。

问题 3:事务超时

错误信息

Transaction timeout

解决方案

// 增加超时时间(默认 60 秒)
@GlobalTransactional(
    timeoutMills = 300000  // 5 分钟
)
public void longRunningTransaction() {
    // 长事务逻辑
}

问题 4:XID 未传播

现象:下游服务没有加入全局事务

解决方案

// 检查拦截器配置
@Configuration
public class SeataConfig {
    
    @Bean
    public SeataFeignInterceptor seataFeignInterceptor() {
        return new SeataFeignInterceptor();
    }
    
    @Bean
    public SeataHandlerInterceptor seataHandlerInterceptor() {
        return new SeataHandlerInterceptor();
    }
}

5.3 监控与运维

Seata Console

Seata 提供了 Web 控制台,用于监控全局事务:

# 下载 Seata Console
wget https://github.com/seata/seata/releases/download/v1.7.1/seata-console-1.7.1.zip

# 启动
unzip seata-console-1.7.1.zip
cd seata-console-1.7.1
sh bin/seata-console.sh

# 访问
http://localhost:7091

功能特性

  • 全局事务列表
  • 事务详情查看
  • 强制回滚
  • 事务日志查询

Prometheus + Grafana

监控指标

# application.yml
seata:
  metrics:
    enabled: true
    registry-type: compact
    exporter-list: prometheus
    exporter-prometheus-port: 9898

关键指标

指标 说明
seata_transaction_total 全局事务总数
seata_transaction_committed_total 已提交事务数
seata_transaction_rollbacked_total 已回滚事务数
seata_transaction_active_count 活跃事务数
seata_resource_lock_total 锁竞争次数

5.4 与 Spring Cloud Alibaba 整合

完整配置示例

父工程

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.8.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

微服务模块

<dependencies>
    <!-- Spring Cloud Alibaba -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
    <!-- Seata -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.2</version>
    </dependency>
</dependencies>

配置文件

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: nacos-cluster:8848

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: nacos-cluster:8848
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: nacos-cluster:8848
      group: SEATA_GROUP

总结

Seata 作为一款优秀的分布式事务框架,极大地简化了分布式事务的开发难度。本文从基础概念、快速上手、进阶实战、原理深入、生产实践五个维度进行了全面讲解。

核心要点回顾

  1. 四种模式选择

    • AT 模式:零侵入,适合大多数场景
    • TCC 模式:高性能,需要代码侵入
    • SAGA 模式:长事务,流程编排
    • XA 模式:强一致,性能较低
  2. AT 模式原理

    • 一阶段:记录前后镜像(undo_log)
    • 二阶段:异步删除或反向回滚
  3. 生产实践

    • 高可用部署(集群 + 数据库存储)
    • 性能优化(连接池、异步提交)
    • 监控运维(Console + Prometheus)

学习路径建议

入门:AT 模式 → 理解全局事务概念
  ↓
进阶:TCC 模式 → 掌握补偿事务
  ↓
深入:源码分析 → 理解事务协调原理
  ↓
实践:生产部署 → 高可用与性能优化

参考资料


作者提示:本文基于 Seata 1.7.x / 2.x 版本编写,部分配置在不同版本可能有所差异,请以官方文档为准。如有疑问,欢迎在评论区留言讨论。

动物装饰