Seata 分布式事务实战:从 AT 模式到阿里云 GTS 方案

简介: 微服务拆分后,跨服务的数据一致性成为最棘手的难题。Seata 作为阿里巴巴开源的分布式事务框架,提供了 AT、TCC、Saga、XA 四种事务模式。阿里云 GTS(全局事务服务)是 Seata 的云托管增强版。本文从电商下单场景出发,实战演示 Seata AT 模式的完整接入流程(订单→库存→账户三服务事务),对比 AT/TCC/Saga 的适用场景,并介绍阿里云 GTS 托管方案和选型建议。

1. 场景:下单容易,一致性难

1.1 一个真实的电商下单困境

在我负责的一个电商平台项目中,拆分微服务后遇到了经典的一致性问题。
下单流程涉及三个服务:订单服务创建订单、库存服务扣减库存、账户服务扣减余额。
任何一个步骤失败,其他步骤必须全部回滚,否则就会出现"扣了钱没发货"或"发了货没扣钱"的资损。

单体应用里,一个本地事务就能搞定。拆成微服务后,每个服务有独立数据库,
本地事务无法覆盖跨库操作,数据一致性成了最大的挑战。

1.2 分布式事务的三大挑战

挑战一:网络分区
服务间通过 HTTP/RPC 调用,网络不可靠。库存服务超时了,到底是成功了还是失败了?
你无法确定,只能重试或补偿。

挑战二:部分失败
订单创建成功,库存扣减成功,账户余额不足——前两个操作需要回滚。
部分失败是分布式系统中最难处理的场景。

挑战三:超时重试
调用超时后自动重试,可能导致重复扣减。幂等性设计成为必须。

1.3 下单流程中的事务问题

下面这张图展示了电商下单流程中,库存扣减失败时的数据不一致问题:

003-seata-gts-distributed-transaction_diagram_1.png

没有分布式事务保障,下单失败后订单记录残留,需要人工对账清理。
在生产环境中,这种不一致每天可能产生上百条脏数据。

2. Seata 核心概念与架构

2.1 三大核心角色

Seata 的架构围绕三个核心角色展开:

角色 全称 职责
TC Transaction Coordinator 事务协调者,维护全局事务和分支事务的状态,驱动提交或回滚
TM Transaction Manager 事务管理器,定义全局事务的范围,开启/提交/回滚全局事务
RM Resource Manager 资源管理器,管理分支事务的资源,向 TC 注册分支事务

简单理解:TC 是裁判,TM 是发起方,RM 是参与者。
TC 独立部署(Seata Server),TM 和 RM 嵌入各微服务中。

2.2 全局事务执行流程

Seata 的全局事务基于改进的两阶段提交(2PC)协议:

第一阶段(Branch 阶段)
各 RM 执行业务 SQL 并生成 undo_log,向 TC 汇报状态。

第二阶段(Commit/Rollback 阶段)
TC 根据所有分支的状态决定提交或回滚。
提交时异步清理 undo_log,回滚时根据 undo_log 反向补偿。

003-seata-gts-distributed-transaction_diagram_2.png

2.3 四种事务模式对比

Seata 提供四种事务模式,适用于不同场景:

维度 AT 模式 TCC 模式 Saga 模式 XA 模式
侵入性 无侵入(自动补偿) 高侵入(三个接口) 中侵入(正补偿) 无侵入
性能 高(一阶段提交) 高(资源预留) 最高(无锁) 低(两阶段锁)
一致性 最终一致 最终一致 最终一致 强一致
适用场景 通用业务 高并发资金 长流程编排 强一致要求
隔离性 全局锁 业务隔离 无隔离 数据库隔离

选型建议

  • 大多数场景选 AT 模式,零侵入、上手快
  • 资金类高并发场景选 TCC 模式,避免全局锁竞争
  • 跨公司长流程编排选 Saga 模式
  • 对一致性要求极高选 XA 模式(性能有代价)

3. Seata Server 部署

3.1 部署模式选择

模式 适用场景 存储方式 高可用
单机 file 本地开发调试 文件存储
单机 db 测试环境 数据库存储
集群 db 生产环境 数据库存储
集群 redis 高性能生产 Redis 存储

开发环境用 file 模式最简单,生产环境必须用 db 或 redis 模式保证事务日志持久化。

3.2 Docker Compose 部署 Seata Server

生产环境推荐使用容器化部署,方便运维和扩容。
以下 docker-compose 配置包含 Seata Server 和 MySQL 存储的完整部署方案。

# docker-compose.yml — Seata Server 集群部署
version: '3.8'
services:
  seata-server:
    image: seataio/seata-server:1.8.0
    container_name: seata-server
    ports:
      - "8091:8091"
      - "7091:7091"
    environment:
      - SEATA_IP=你的服务器IP
      - SEATA_PORT=8091
    volumes:
      - ./seata-config:/seata-server/resources
    depends_on:
      - seata-mysql
    restart: always
    networks:
      - seata-net

  seata-mysql:
    image: mysql:8.0
    container_name: seata-mysql
    environment:
      MYSQL_ROOT_PASSWORD: seata_root_pwd
      MYSQL_DATABASE: seata
      MYSQL_USER: seata
      MYSQL_PASSWORD: seata_pwd
    ports:
      - "3307:3306"
    volumes:
      - ./mysql-data:/var/lib/mysql
      - ./sql/init-seata.sql:/docker-entrypoint-initdb.d/init.sql
    restart: always
    networks:
      - seata-net

networks:
  seata-net:
    driver: bridge

3.3 Seata Server 配置

Seata Server 的存储模式和注册中心配置是生产部署的关键。
file.conf 控制事务日志存储,registry.conf 控制服务注册发现。

# file.conf — Seata Server 存储配置(db 模式)
store {
  mode = "db"
  db {
    datasource = "druid"
    dbType = "mysql"
    driverClassName = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://seata-mysql:3306/seata?rewriteBatchedStatements=true"
    user = "seata"
    password = "seata_pwd"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    distributedLockTable = "distributed_lock"
    queryLimit = 100
    maxWait = 5000
  }
}
# registry.conf — 注册中心配置(Nacos)
registry {
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    username = "nacos"
    password = "nacos"
  }
}

3.4 Seata Server 数据库初始化

db 模式需要创建 Seata 自身的事务日志表,否则 Server 启动报错。

-- init-seata.sql — Seata Server 数据库初始化脚本

-- 全局事务表
CREATE TABLE IF NOT EXISTS `global_table`
(
  `xid`
  VARCHAR
(
  128
) NOT NULL,
  `transaction_id` BIGINT,
  `status` TINYINT NOT NULL,
  `application_id` VARCHAR
(
  32
),
  `transaction_service_group` VARCHAR
(
  32
),
  `transaction_name` VARCHAR
(
  128
),
  `timeout` INT,
  `begin_time` BIGINT,
  `application_data` VARCHAR
(
  2000
),
  `gmt_create` DATETIME,
  `gmt_modified` DATETIME,
  PRIMARY KEY
(
  `xid`
),
  KEY `idx_status_gmt_modified`
(
  `status`,
  `gmt_modified`
),
  KEY `idx_transaction_id`
(
  `transaction_id`
)
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

-- 分支事务表
CREATE TABLE IF NOT EXISTS `branch_table`
(
  `branch_id`
  BIGINT
  NOT
  NULL,
  `xid`
  VARCHAR
(
  128
) NOT NULL,
  `transaction_id` BIGINT,
  `resource_group_id` VARCHAR
(
  32
),
  `resource_id` VARCHAR
(
  256
),
  `branch_type` VARCHAR
(
  8
),
  `status` TINYINT,
  `client_id` VARCHAR
(
  64
),
  `application_data` VARCHAR
(
  2000
),
  `gmt_create` DATETIME
(
  6
),
  `gmt_modified` DATETIME
(
  6
),
  PRIMARY KEY
(
  `branch_id`
),
  KEY `idx_xid`
(
  `xid`
)
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

-- 全局锁表
CREATE TABLE IF NOT EXISTS `lock_table`
(
  `row_key`
  VARCHAR
(
  128
) NOT NULL,
  `xid` VARCHAR
(
  128
),
  `transaction_id` BIGINT,
  `branch_id` BIGINT NOT NULL,
  `resource_id` VARCHAR
(
  256
),
  `table_name` VARCHAR
(
  32
),
  `pk` VARCHAR
(
  36
),
  `status` TINYINT NOT NULL DEFAULT '0',
  `gmt_create` DATETIME,
  `gmt_modified` DATETIME,
  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;

-- 分布式锁表
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
  `lock_key`
  CHAR
(
  20
) NOT NULL,
  `lock_value` VARCHAR
(
  20
) NOT NULL,
  `expire` BIGINT,
  PRIMARY KEY
(
  `lock_key`
)
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire)
VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire)
VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire)
VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire)
VALUES ('TxTimeoutCheck', ' ', 0);

4. AT 模式实战:订单-库存-账户

4.1 为什么 AT 模式最适合入门

AT 模式是 Seata 最常用的事务模式,核心优势是零侵入

  • 不需要修改业务 SQL,Seata 自动拦截并生成回滚日志
  • 一阶段直接提交本地事务,不持有全局锁,性能好
  • 回滚时根据 undo_log 自动反向补偿,开发者无感知

AT 模式的工作原理:

  1. 拦截业务 SQL,解析语义
  2. 执行前查询修改前的数据(before image)
  3. 执行业务 SQL
  4. 查询修改后的数据(after image)
  5. 生成 undo_log 并写入 undo_log 表
  6. 一阶段提交,释放本地锁(保留全局锁)

4.2 项目依赖配置

Spring Cloud Alibaba 集成 Seata 需要引入 seata-spring-boot-starter,
版本必须与 Seata Server 版本一致,否则协议不兼容。

<!-- pom.xml — 核心依赖 -->
<properties>
  <spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>
  <seata.version>1.8.0</seata.version>
</properties>

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

<dependencies>
<!-- Seata Starter -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- Seata 核心(版本与 Server 一致) -->
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>${seata.version}</version>
</dependency>
</dependencies>

4.3 各服务 Seata 配置

每个参与分布式事务的微服务都需要配置 Seata 事务组和服务组映射, 确保 TM 和 RM 能正确连接到 TC 并归属同一个事务组。

# application.yml — 订单服务 Seata 配置
seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP

4.4 undo_log 表结构

AT 模式的核心是 undo_log 表,Seata 通过它实现自动补偿。
每个参与分布式事务的数据库都必须创建这张表,否则回滚时找不到补偿数据。

-- undo_log 表 — 每个业务库都必须创建
CREATE TABLE IF NOT EXISTS `undo_log`
(
  `branch_id`
  BIGINT
  NOT
  NULL
  COMMENT
  '分支事务ID',
  `xid`
  VARCHAR
(
  128
) NOT NULL COMMENT '全局事务ID',
  `context` VARCHAR
(
  128
) NOT NULL COMMENT '上下文',
  `rollback_info` LONGBLOB NOT NULL COMMENT '回滚数据',
  `log_status` INT
(
  11
) NOT NULL COMMENT '日志状态',
  `log_created` DATETIME
(
  6
) NOT NULL COMMENT '创建时间',
  `log_modified` DATETIME
(
  6
) NOT NULL COMMENT '修改时间',
  PRIMARY KEY
(
  `branch_id`
),
  KEY `idx_xid`
(
  `xid`
)
  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='AT 模式回滚日志表';

4.5 订单服务核心代码

订单服务是分布式事务的发起方(TM),通过 @GlobalTransactional 注解 开启全局事务。该注解是 Seata AT 模式最核心的入口。

// OrderService.java — 订单服务(TM 角色)
@Service
public class OrderService {
   

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private StockFeignClient stockFeignClient;

    @Autowired
    private AccountFeignClient accountFeignClient;

    /**
     * 下单方法 — 开启全局事务
     * @GlobalTransactional 标注的方法即为全局事务入口
     * rollbackFor 指定回滚异常类型
     * timeoutMills 设置全局事务超时时间
     */
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class, timeoutMills = 60000)
    public Order createOrder(OrderDTO orderDTO) {
   
        // 1. 创建订单
        Order order = new Order();
        order.setUserId(orderDTO.getUserId());
        order.setCommodityCode(orderDTO.getCommodityCode());
        order.setCount(orderDTO.getCount());
        order.setMoney(orderDTO.getMoney());
        order.setStatus("INIT");
        orderMapper.insert(order);

        // 2. 扣减库存(远程调用库存服务)
        stockFeignClient.deduct(orderDTO.getCommodityCode(), orderDTO.getCount());

        // 3. 扣减余额(远程调用账户服务)
        accountFeignClient.debit(orderDTO.getUserId(), orderDTO.getMoney());

        // 4. 更新订单状态
        order.setStatus("SUCCESS");
        orderMapper.updateById(order);

        return order;
    }
}

4.6 库存服务和账户服务代码

库存服务和账户服务是分支事务参与者(RM),不需要加 @GlobalTransactional。
它们只需要正常执行本地业务 SQL,Seata 会自动拦截并管理分支事务。

// StockService.java — 库存服务(RM 角色)
@Service
public class StockService {
   

    @Autowired
    private StockMapper stockMapper;

    /**
     * 扣减库存 — RM 分支事务
     * Seata 自动拦截 SQL,生成 undo_log
     * 如果全局事务回滚,自动根据 undo_log 补偿
     */
    public void deduct(String commodityCode, int count) {
   
        Stock stock = stockMapper.findByCommodityCode(commodityCode);
        if (stock == null) {
   
            throw new RuntimeException("商品不存在: " + commodityCode);
        }
        if (stock.getCount() < count) {
   
            throw new RuntimeException("库存不足,当前库存: " + stock.getCount());
        }
        stock.setCount(stock.getCount() - count);
        stockMapper.updateById(stock);
    }
}
// AccountService.java — 账户服务(RM 角色)
@Service
public class AccountService {
   

    @Autowired
    private AccountMapper accountMapper;

    /**
     * 扣减余额 — RM 分支事务
     * 余额不足时抛出异常,触发全局事务回滚
     */
    public void debit(Long userId, BigDecimal money) {
   
        Account account = accountMapper.findByUserId(userId);
        if (account == null) {
   
            throw new RuntimeException("账户不存在: " + userId);
        }
        if (account.getMoney().compareTo(money) < 0) {
   
            throw new RuntimeException("余额不足,当前余额: " + account.getMoney());
        }
        account.setMoney(account.getMoney().subtract(money));
        accountMapper.updateById(account);
    }
}

4.7 Feign 客户端传递 xid

全局事务 ID(xid)需要在服务间传递,否则下游服务无法加入全局事务。
Seata 默认通过拦截器自动传递 xid,但 Feign 调用需要额外配置。

// SeataFeignConfig.java — Feign 传递 xid 配置
@Configuration
public class SeataFeignConfig {
   

    @Bean
    public RequestInterceptor seataFeignInterceptor() {
   
        return new RequestInterceptor() {
   
            @Override
            public void apply(RequestTemplate template) {
   
                String xid = RootContext.getXID();
                if (StringUtils.isNotBlank(xid)) {
   
                    template.header(RootContext.KEY_XID, xid);
                }
            }
        };
    }
}

5. TCC 模式实战:高并发场景

5.1 AT 模式的局限

AT 模式虽然零侵入,但在高并发场景下存在全局锁竞争问题:

  • 两个全局事务同时修改同一行数据,后到的必须等待前一个释放全局锁
  • 热点商品(如秒杀)的库存行会成为严重的锁竞争瓶颈
  • 全局锁持有时间 = 整个全局事务的执行时间,锁粒度太粗

TCC 模式通过资源预留解决全局锁问题:

  • Try 阶段只冻结资源,不真正扣减
  • Confirm 阶段确认扣减,Cancel 阶段释放冻结
  • 全局锁的粒度从"整行"缩小到"冻结字段"

5.2 TCC 三阶段详解

阶段 动作 说明
Try 资源预留 冻结库存/余额,不真正扣减
Confirm 确认提交 扣减冻结资源,必须幂等
Cancel 取消补偿 释放冻结资源,必须幂等

5.3 TCC 模式代码示例

TCC 模式需要手动编写 Try/Confirm/Cancel 三个方法,
通过 @TwoPhaseBusinessAction 注解声明三阶段方法映射。

// AccountTccService.java — 账户服务 TCC 模式
@Service
public class AccountTccService {
   

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private FreezeMapper freezeMapper;

    /**
     * Try 阶段 — 冻结余额
     * BusinessActionContextParameter 传递参数到 Confirm/Cancel
     */
    @TwoPhaseBusinessAction(
        name = "accountTcc",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    public boolean tryDebit(
            @BusinessActionContextParameter(paramName = "userId") Long userId,
            @BusinessActionContextParameter(paramName = "money") BigDecimal money) {
   

        // 1. 查询账户
        Account account = accountMapper.findByUserId(userId);
        if (account == null) {
   
            throw new RuntimeException("账户不存在");
        }

        // 2. 检查可用余额(总余额 - 冻结金额)
        BigDecimal available = account.getMoney().subtract(
            account.getFreezeMoney() != null ? account.getFreezeMoney() : BigDecimal.ZERO
        );
        if (available.compareTo(money) < 0) {
   
            throw new RuntimeException("可用余额不足");
        }

        // 3. 冻结余额(不是真正扣减)
        account.setFreezeMoney(
            (account.getFreezeMoney() != null ? account.getFreezeMoney() : BigDecimal.ZERO)
                .add(money)
        );
        accountMapper.updateById(account);

        // 4. 记录冻结流水(用于幂等和空回滚判断)
        FreezeRecord record = new FreezeRecord();
        record.setUserId(userId);
        record.setMoney(money);
        record.setStatus("FROZEN");
        freezeMapper.insert(record);

        return true;
    }

    /**
     * Confirm 阶段 — 确认扣减
     * 必须幂等:重复调用不能重复扣减
     */
    public boolean confirm(BusinessActionContext context) {
   
        Long userId = context.getActionContext("userId", Long.class);
        BigDecimal money = context.getActionContext("money", BigDecimal.class);

        // 1. 查询冻结记录(幂等判断)
        FreezeRecord record = freezeMapper.findByUserIdAndStatus(userId, "FROZEN");
        if (record == null) {
   
            // 已经确认过,直接返回成功
            return true;
        }

        // 2. 真正扣减余额
        Account account = accountMapper.findByUserId(userId);
        account.setMoney(account.getMoney().subtract(money));
        account.setFreezeMoney(account.getFreezeMoney().subtract(money));
        accountMapper.updateById(account);

        // 3. 更新冻结记录状态
        record.setStatus("CONFIRMED");
        freezeMapper.updateById(record);

        return true;
    }

    /**
     * Cancel 阶段 — 释放冻结
     * 必须处理空回滚:Try 未执行但 Cancel 被调用
     */
    public boolean cancel(BusinessActionContext context) {
   
        Long userId = context.getActionContext("userId", Long.class);
        BigDecimal money = context.getActionContext("money", BigDecimal.class);

        // 1. 查询冻结记录
        FreezeRecord record = freezeMapper.findByUserIdAndStatus(userId, "FROZEN");

        if (record == null) {
   
            // 空回滚:Try 未执行,Cancel 被调用
            // 插入一条标记记录,防止悬挂
            FreezeRecord emptyRecord = new FreezeRecord();
            emptyRecord.setUserId(userId);
            emptyRecord.setMoney(BigDecimal.ZERO);
            emptyRecord.setStatus("CANCELLED");
            freezeMapper.insert(emptyRecord);
            return true;
        }

        // 2. 释放冻结余额
        Account account = accountMapper.findByUserId(userId);
        account.setFreezeMoney(account.getFreezeMoney().subtract(money));
        accountMapper.updateById(account);

        // 3. 更新冻结记录状态
        record.setStatus("CANCELLED");
        freezeMapper.updateById(record);

        return true;
    }
}

5.4 空回滚和悬挂问题

空回滚:Try 未执行但 Cancel 被调用。
原因:Try 阶段超时,TC 决定回滚,但 Try 的网络请求还在路上。
处理:Cancel 中判断冻结记录是否存在,不存在则插入标记记录直接返回。

悬挂:Cancel 先于 Try 执行。
原因:空回滚后,Try 请求才到达并执行成功,资源被冻结但永远不会释放。
处理:Try 中先查询是否已有 Cancel 标记记录,有则拒绝执行。

// 防悬挂:Try 方法开头增加判断
public boolean tryDebit(Long userId, BigDecimal money) {
   
    // 检查是否已经 Cancel 过(防悬挂)
    FreezeRecord cancelled = freezeMapper.findByUserIdAndStatus(userId, "CANCELLED");
    if (cancelled != null) {
   
        // 已经 Cancel 过,拒绝执行 Try
        return false;
    }
    // ... 正常 Try 逻辑
}

6. 阿里云 GTS 托管方案

6.1 GTS 是什么

阿里云 GTS(Global Transaction Service,全局事务服务)是 Seata 的云托管增强版。
它将 Seata Server 的运维工作交给阿里云,开发者只需关注业务代码。

GTS 核心优势

  • 免运维:无需部署和维护 Seata Server 集群
  • 高可用:阿里云提供多可用区容灾,SLA 99.99%
  • 性能增强:基于阿里云内部优化,吞吐量更高
  • 兼容 Seata:客户端代码无需修改,只改配置

6.2 GTS vs 自建 Seata Server 对比

维度 自建 Seata Server 阿里云 GTS
运维成本 需要专人维护集群、升级、监控 零运维,阿里云托管
高可用 需要自行搭建多节点 + 负载均衡 内置多可用区容灾
性能上限 取决于自建集群规模 云端弹性扩缩容
版本升级 手动升级,有兼容风险 阿里云自动升级
故障恢复 需要自建监控和告警 阿里云自动故障转移
费用 ECS 实例费 + 运维人力 按 TPS 和事务量计费
适用场景 有运维能力的中大型团队 追求稳定和效率的团队

6.3 GTS 接入流程

步骤一:开通阿里云 GTS 服务,获取接入点地址和 AccessKey。

步骤二:修改客户端配置,将 TC 地址指向 GTS 接入点。

GTS 接入的核心就是将 Seata 客户端的 TC 地址从自建 Server
切换到阿里云 GTS 接入点,业务代码无需任何修改。

# application.yml — GTS 接入配置
seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: gts-default
    grouplist:
      # 自建 Server 地址
      # default: 127.0.0.1:8091
      # GTS 接入点地址(在 GTS 控制台获取)
      gts-default: gts-xxx.cn-hangzhou.aliyuncs.com:8091
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP

步骤三:添加 GTS 客户端依赖。

<!-- pom.xml — GTS 客户端依赖 -->
<dependency>
  <groupId>com.aliyun.gts</groupId>
  <artifactId>gts-client</artifactId>
  <version>2.0.0</version>
</dependency>

步骤四:业务代码无需修改,@GlobalTransactional 注解照常使用。

6.4 成本对比

以日均 10 万笔分布式事务为例:

项目 自建 Seata Server 阿里云 GTS
ECS 实例(3 节点) 约 600 元/月 0
RDS MySQL(存储日志) 约 300 元/月 0
运维人力 约 5000 元/月 0
GTS 服务费 0 约 800 元/月
合计 约 5900 元/月 约 800 元/月

💡 以上费用仅为估算,实际费用以阿里云官网价格为准。事务量越大,GTS 的成本优势越明显。

7. 避坑指南

坑 1:undo_log 表忘记创建

现象:AT 模式下,全局事务回滚时报错 Table 'xxx.undo_log' doesn't exist
导致数据无法自动补偿,产生脏数据。

原因:AT 模式依赖 undo_log 表记录回滚数据,每个参与分布式事务的数据库
都必须创建这张表。很多开发者只在订单库创建了,忘记在库存库和账户库创建。

解决:在每个业务库中执行 undo_log 建表语句(见第 4.4 节),
并在 CI/CD 流程中加入检查脚本,确保所有库都有这张表。

坑 2:AT 模式全局锁导致死锁

现象:两个全局事务互相等待对方释放全局锁,最终超时回滚。
日志中出现 Global lock wait timeout 错误。

原因:AT 模式在全局事务提交前持有全局锁。如果事务 A 和事务 B
同时修改同一行数据,先拿到锁的事务不提交,后到的只能等待。

解决

  • 缩小事务范围,减少全局锁持有时间
  • 设置合理的全局锁重试次数和间隔:client.rm.lock.retryInterval=10client.rm.lock.retryTimes=30
  • 高并发场景切换为 TCC 模式

坑 3:TCC 空回滚和悬挂问题

现象:TCC 模式下,Cancel 方法先于 Try 方法执行,导致资源状态异常。
或者 Try 方法在 Cancel 之后执行成功,冻结的资源永远无法释放。

原因:Try 阶段网络超时,TC 触发回滚调用 Cancel,但 Try 请求还在路上。

解决:参见第 5.4 节的空回滚和悬挂处理方案,核心是:

  • Cancel 中判断 Try 是否执行过,未执行则记录标记
  • Try 中判断是否已经 Cancel 过,已 Cancel 则拒绝执行

坑 4:Seata 与多数据源的兼容性

现象:项目中使用了动态数据源(如 MyBatis-Plus 的 dynamic-datasource),
Seata 的数据源代理不生效,分布式事务失效。

原因:Seata 通过代理 DataSource 来拦截 SQL。如果动态数据源在 Seata
代理之后又包装了一层,Seata 拦截不到实际的 SQL 执行。

解决:确保 Seata 的 DataSourceProxy 在数据源链的最外层,
或者使用 Seata 官方提供的 SeataAutoDataSourceProxyCreator:

# application.yml — 多数据源场景配置
seata:
  enable-auto-data-source-proxy: true
  data-source-proxy-mode: AT

坑 5:分布式事务的超时配置

现象:全局事务执行时间较长(如调用第三方支付接口),超过默认超时时间后
被 TC 强制回滚,但业务实际已经执行成功。

原因:Seata 全局事务默认超时 60 秒,长事务场景容易超时。

解决

  • 通过 @GlobalTransactional 的 timeoutMills 属性调整超时时间
  • 全局配置默认超时:service.default.grouplist.timeout=120000
// 调整单次事务超时时间
@GlobalTransactional(timeoutMills = 120000)
public Order createOrder(OrderDTO orderDTO) {
   
    // ...
}

8. 总结与下一步

核心要点回顾

要点 说明
Seata 架构 TC/TM/RM 三角色,改进版 2PC 协议
AT 模式 零侵入、自动补偿,适合大多数场景
TCC 模式 手动三阶段,适合高并发资金场景
undo_log AT 模式核心,每个业务库必须创建
全局锁 AT 模式的性能瓶颈,高并发考虑 TCC
空回滚/悬挂 TCC 模式必须处理的边界问题
阿里云 GTS Seata 云托管版,免运维、高可用

选型决策

  • 通用业务 → AT 模式(80% 场景)
  • 资金高并发 → TCC 模式
  • 长流程编排 → Saga 模式
  • 强一致要求 → XA 模式
  • 不想运维 → 阿里云 GTS

系列文章预告

下一篇将实战 RocketMQ 消息队列在微服务中的应用,
包括顺序消息、事务消息、延迟消息的生产级用法,
以及阿里云 MQ 托管方案对比,敬请关注。

相关文章
|
7天前
|
人工智能 JSON 自然语言处理
让教学更智慧:用阿里云百炼工作流,自动生成中小学教材内容#小有可为#有温度的AI
通过可视化工作流编排,将大模型推理能力转化为标准化的教学内容生成引擎。教师只需输入教材标题和适用学段,即可自动获得结构完整、符合课程标准的章节内容,大幅降低备课门槛,助力教育资源均衡化。
474 123
|
8天前
|
人工智能 定位技术 SEO
我学 GEO 第 15 天:终于知道AI GEO该如何做?
我是暴走的莉莉酱,边旅行边研究AI GEO的数字游民。专注普通人如何提升“AI可见度”——让AI在回答用户问题时准确识别、理解并推荐你。不讲玄学,只做可测、可调、可持续的GEO实践。
451 127
|
16天前
|
Linux 程序员 数据格式
【2026最新】Notepad++下载、安装和使用一篇搞定(附中文版安装包)
Notepad++ 是一款免费开源、轻量高效的 Windows 文本编辑器,支持 C/Python/HTML 等 80+ 语言语法高亮、代码折叠、正则替换、编码转换及插件扩展,专为程序员与文本处理用户打造,完美替代系统记事本。(239字)
|
11天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
781 5
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
|
3天前
|
人工智能 安全 Cloud Native
Higress 新发布:AI Gateway 能力增强,Gateway API 及其推理扩展持续打磨
增强 AI 网关能力,持续打磨 Gateway API 及其推理扩展。
299 122
|
3天前
|
消息中间件 存储 Kafka
Kafka 原生消息入湖能力上线!一键打通实时流与数据湖
阿里云消息队列 Kafka 版正式上线原生消息入湖能力。
249 121
|
8天前
|
缓存 人工智能 运维
阿里云618百炼大模型Qwen3.7-Max功能、免费试用、订阅计费、配置接入详解
Qwen3.7-MAX是阿里云百炼平台推出的通义千问3.7系列旗舰大语言模型,专为智能体时代复杂任务打造,依托阿里云全域算力与自研技术,在逻辑推理、长文本处理、代码工程、长周期自主执行等领域达到行业顶尖水平。2026年618期间,该模型推出多重免费试用权益、按量计费5折、订阅套餐优惠等专属福利,覆盖个人开发者、团队与企业全场景需求,以下从核心功能、免费试用、订阅计费、配置接入四方面展开详细解析。
464 124