1. 场景:下单容易,一致性难
1.1 一个真实的电商下单困境
在我负责的一个电商平台项目中,拆分微服务后遇到了经典的一致性问题。
下单流程涉及三个服务:订单服务创建订单、库存服务扣减库存、账户服务扣减余额。
任何一个步骤失败,其他步骤必须全部回滚,否则就会出现"扣了钱没发货"或"发了货没扣钱"的资损。
单体应用里,一个本地事务就能搞定。拆成微服务后,每个服务有独立数据库,
本地事务无法覆盖跨库操作,数据一致性成了最大的挑战。
1.2 分布式事务的三大挑战
挑战一:网络分区
服务间通过 HTTP/RPC 调用,网络不可靠。库存服务超时了,到底是成功了还是失败了?
你无法确定,只能重试或补偿。
挑战二:部分失败
订单创建成功,库存扣减成功,账户余额不足——前两个操作需要回滚。
部分失败是分布式系统中最难处理的场景。
挑战三:超时重试
调用超时后自动重试,可能导致重复扣减。幂等性设计成为必须。
1.3 下单流程中的事务问题
下面这张图展示了电商下单流程中,库存扣减失败时的数据不一致问题:

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

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 模式的工作原理:
- 拦截业务 SQL,解析语义
- 执行前查询修改前的数据(before image)
- 执行业务 SQL
- 查询修改后的数据(after image)
- 生成 undo_log 并写入 undo_log 表
- 一阶段提交,释放本地锁(保留全局锁)
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=10、client.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 托管方案对比,敬请关注。