前言
在微服务架构中,一次业务操作往往需要跨越多个服务、多个数据库。传统的 ACID 事务只能保证单库的原子性,而跨服务调用时,"部分成功、部分失败"的问题会直接导致数据不一致。
举个典型场景:下单扣库存 + 扣余额,如果扣完库存后,扣余额的服务宕机了,库存已减但订单没创建,数据就乱了。
Seata 是阿里巴巴开源的分布式事务解决方案,目前已进入 Apache 孵化,是微服务体系里处理分布式事务的事实标准之一。本文将带你从零开始,在 Spring Boot 3.x + Nacos 环境下集成 Seata,覆盖最常用的 AT 模式 和 TCC 模式,并给出完整踩坑记录。
一、Seata 核心概念速览
在动手之前,先理清几个关键术语:
| 角色 | 说明 |
|---|---|
| TC(Transaction Coordinator) | 事务协调者,独立部署的 Seata Server,维护全局事务状态 |
| TM(Transaction Manager) | 事务管理器,标注 @GlobalTransactional 的服务,负责开启/提交/回滚全局事务 |
| RM(Resource Manager) | 资源管理器,操作具体数据库的服务,向 TC 汇报分支事务状态 |
1.1 AT 模式(推荐入门)
AT 模式是 Seata 的默认模式,对业务代码无侵入:
- 一阶段:业务 SQL 执行,Seata 拦截并生成
undo_log(回滚日志) - 二阶段提交:删除 undo_log,全局提交
- 二阶段回滚:读取 undo_log,执行逆向 SQL,还原数据
核心要求:每个业务库都要建
undo_log表。
1.2 TCC 模式(高性能场景)
TCC 需要业务方自己实现三个方法:
Try:资源预留(锁定/冻结)Confirm:真正执行业务逻辑Cancel:释放预留资源
TCC 无需 undo_log,性能更高,但对业务有一定侵入性。
二、环境准备
2.1 版本说明
| 组件 | 版本 |
|---|---|
| Spring Boot | 3.2.x |
| Spring Cloud | 2023.0.x |
| Spring Cloud Alibaba | 2023.0.1.0 |
| Seata Server | 2.1.0 |
| Nacos | 2.3.x |
| MySQL | 8.0+ |
| JDK | 17+ |
2.2 部署 Seata Server
从 Seata Releases 下载 seata-server-2.1.0.tar.gz,解压后修改配置:
conf/application.yml(核心配置片段)
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
data-id: seataServer.properties
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf8&useSSL=false
user: root
password: your_password
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
2.3 初始化 Seata 数据库
在 MySQL 中创建 seata 数据库,执行官方 SQL 脚本(script/server/db/mysql.sql),主要创建:
global_table:全局事务表branch_table:分支事务表lock_table:全局锁表
2.4 每个业务库创建 undo_log 表(AT 模式必须)
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
三、Spring Boot 3.x 集成 Seata
3.1 引入依赖
<dependencies>
<!-- Spring Cloud Alibaba Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- Nacos 注册与配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- OpenFeign 服务调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.21</version>
</dependency>
</dependencies>
注意:spring-cloud-alibaba 的 BOM 版本需要和 Spring Boot 3.x 对应,使用 2023.0.1.0。
3.2 application.yml 配置
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
alibaba:
seata:
tx-service-group: my_tx_group # 事务组名,需与 Seata Server 配置对应
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default # 映射到 Seata Server 的集群名
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
application: seata-server
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
data-id: seataServer.properties
data-source-proxy-mode: AT # AT 模式(TCC 模式下可省略)
四、AT 模式实战:下单 + 扣库存
4.1 业务场景
order-service(订单服务):创建订单stock-service(库存服务):扣减库存account-service(账户服务):扣减余额
order-service 作为 TM,通过 Feign 调用 stock-service 和 account-service。
4.2 订单服务(TM 端)
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final StockFeignClient stockFeignClient;
private final AccountFeignClient accountFeignClient;
/**
* @GlobalTransactional 开启全局事务,这是 AT 模式的核心注解
* rollbackFor 建议显式指定,避免非受检异常不触发回滚
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
@Override
public void 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(0); // 创建中
orderMapper.insert(order);
// 2. 扣减库存(远程调用)
stockFeignClient.deduct(orderDTO.getCommodityCode(), orderDTO.getCount());
// 3. 扣减账户余额(远程调用)
accountFeignClient.deduct(orderDTO.getUserId(), orderDTO.getMoney());
// 4. 更新订单状态
order.setStatus(1); // 已完成
orderMapper.updateById(order);
log.info("订单创建成功,XID: {}", RootContext.getXID());
}
}
4.3 库存服务(RM 端)
@Service
@RequiredArgsConstructor
public class StockServiceImpl implements StockService {
private final StockMapper stockMapper;
/**
* RM 端无需 @GlobalTransactional,Seata 会自动代理数据源
* 拦截 SQL,生成 before/after image,写入 undo_log
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void deduct(String commodityCode, Integer count) {
Stock stock = stockMapper.selectByCommodityCode(commodityCode);
if (stock == null || stock.getCount() < count) {
throw new RuntimeException("库存不足,commodityCode: " + commodityCode);
}
stock.setCount(stock.getCount() - count);
stockMapper.updateById(stock);
log.info("库存扣减成功,商品: {},扣减: {},剩余: {}",
commodityCode, count, stock.getCount());
}
}
4.4 验证 AT 模式回滚
主动在账户服务中抛出异常,观察库存和订单数据是否自动回滚:
// AccountServiceImpl.java
@Override
public void deduct(Long userId, BigDecimal money) {
// 模拟异常
if (money.compareTo(new BigDecimal("500")) > 0) {
throw new RuntimeException("余额不足!");
}
// ... 扣减逻辑
}
测试结果:当账户扣减失败时,已执行的库存扣减和订单创建会被 Seata 自动回滚,undo_log 中的逆向 SQL 被执行,数据完整恢复。
五、TCC 模式实战:资金转账
AT 模式依赖数据库 undo_log,在高并发场景下会有性能瓶颈。TCC 模式通过业务层面的资源预留避免了这个问题。
5.1 TCC 接口定义
@LocalTCC
public interface AccountTccService {
/**
* Try 阶段:冻结资金(资源预留)
* @TwoPhaseBusinessAction 声明这是一个 TCC 接口
* commitMethod 对应 Confirm,rollbackMethod 对应 Cancel
*/
@TwoPhaseBusinessAction(
name = "accountTccService",
commitMethod = "confirmDeduct",
rollbackMethod = "cancelDeduct"
)
boolean tryDeduct(BusinessActionContext context,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
/**
* Confirm 阶段:真正扣减,需保证幂等
*/
boolean confirmDeduct(BusinessActionContext context);
/**
* Cancel 阶段:释放冻结资金,需保证幂等
*/
boolean cancelDeduct(BusinessActionContext context);
}
5.2 TCC 接口实现
@Service
@RequiredArgsConstructor
@Slf4j
public class AccountTccServiceImpl implements AccountTccService {
private final AccountMapper accountMapper;
private final FreezeRecordMapper freezeRecordMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean tryDeduct(BusinessActionContext context, Long userId, BigDecimal money) {
String xid = context.getXid();
log.info("[TCC-Try] userId: {}, money: {}, xid: {}", userId, money, xid);
// 检查是否已处理(幂等控制)
FreezeRecord record = freezeRecordMapper.selectByXid(xid);
if (record != null) {
log.info("[TCC-Try] 已处理,幂等返回 true");
return true;
}
// 查询账户
Account account = accountMapper.selectByUserId(userId);
if (account == null || account.getMoney().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
// 冻结资金:减少可用余额,增加冻结金额
account.setMoney(account.getMoney().subtract(money));
account.setFreezeMoney(account.getFreezeMoney().add(money));
accountMapper.updateById(account);
// 记录冻结流水(用于 Confirm/Cancel 幂等判断)
FreezeRecord newRecord = new FreezeRecord();
newRecord.setXid(xid);
newRecord.setUserId(userId);
newRecord.setMoney(money);
newRecord.setState(0); // 0: 冻结中
freezeRecordMapper.insert(newRecord);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmDeduct(BusinessActionContext context) {
String xid = context.getXid();
Long userId = Long.parseLong(context.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(context.getActionContext("money").toString());
log.info("[TCC-Confirm] userId: {}, money: {}, xid: {}", userId, money, xid);
// 幂等检查
FreezeRecord record = freezeRecordMapper.selectByXid(xid);
if (record == null || record.getState() == 1) {
log.info("[TCC-Confirm] 已处理,幂等返回 true");
return true;
}
// 清除冻结金额(资金真正扣除)
Account account = accountMapper.selectByUserId(userId);
account.setFreezeMoney(account.getFreezeMoney().subtract(money));
accountMapper.updateById(account);
// 更新流水状态
record.setState(1); // 1: 已确认
freezeRecordMapper.updateById(record);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelDeduct(BusinessActionContext context) {
String xid = context.getXid();
Long userId = Long.parseLong(context.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(context.getActionContext("money").toString());
log.info("[TCC-Cancel] userId: {}, money: {}, xid: {}", userId, money, xid);
// 幂等检查
FreezeRecord record = freezeRecordMapper.selectByXid(xid);
if (record == null) {
// 空回滚:Try 阶段未执行就触发了 Cancel,直接返回成功
log.info("[TCC-Cancel] 空回滚,直接返回 true");
return true;
}
if (record.getState() == 2) {
log.info("[TCC-Cancel] 已回滚,幂等返回 true");
return true;
}
// 释放冻结资金:归还可用余额
Account account = accountMapper.selectByUserId(userId);
account.setMoney(account.getMoney().add(money));
account.setFreezeMoney(account.getFreezeMoney().subtract(money));
accountMapper.updateById(account);
// 更新流水状态
record.setState(2); // 2: 已取消
freezeRecordMapper.updateById(record);
return true;
}
}
六、踩坑记录
坑1:vgroup-mapping 配置不匹配导致连接 Seata Server 失败
现象:启动时报 no available service 'default' found
原因:tx-service-group 配置的值(如 my_tx_group),需要在 Nacos 的 seataServer.properties 中有对应的 service.vgroupMapping.my_tx_group=default 映射,且 default 必须与 Seata Server 的集群名一致。
解决:确保三方一致:
# Nacos 中 seataServer.properties
service.vgroupMapping.my_tx_group=default
# 应用配置
seata.service.vgroup-mapping.my_tx_group: default
坑2:Spring Boot 3.x 下 DataSourceProxy 自动配置冲突
现象:启动时报 No qualifying bean of type 'DataSource' 或 The bean 'dataSource' could not be registered
原因:spring-cloud-starter-alibaba-seata 在 Spring Boot 3.x 下,自动配置类路径有变化,可能导致数据源代理重复注册。
解决:在 application.yml 中显式排除自动配置,并手动配置数据源代理:
spring:
autoconfigure:
exclude:
- com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure
然后手动配置 DataSourceProxy Bean:
@Configuration
public class SeataDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
坑3:TCC 空回滚与悬挂问题
现象:Cancel 在 Try 之前执行(网络延迟场景),或 Try 在 Cancel 后执行(悬挂)
解决策略:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 空回滚 | Try 未执行,Cancel 被调用 | Cancel 中检查冻结记录,不存在则直接返回成功 |
| 幂等 | Confirm/Cancel 被重复调用 | 通过 XID + 状态字段做幂等判断 |
| 悬挂 | Cancel 先执行,Try 后执行 | Cancel 执行时插入一条"已取消"记录,Try 阶段检查到后直接返回失败 |
坑4:MyBatis Plus 与 Seata AT 模式的兼容性
现象:使用 updateById 时,Seata 生成的 undo_log 不完整
原因:MyBatis Plus 动态 SQL 在某些情况下不会更新所有字段,导致 before image 与 after image 不匹配。
解决:关键更新操作使用 updateAllColumns,或者使用 LambdaUpdateWrapper 明确指定更新字段。
七、AT 模式 vs TCC 模式选型建议
| 维度 | AT 模式 | TCC 模式 |
|---|---|---|
| 业务侵入性 | 无侵入,自动代理 | 需要实现 Try/Confirm/Cancel |
| 性能 | 有 undo_log 写入开销 | 高,无 undo_log |
| 适用场景 | 简单业务,快速接入 | 高并发、高性能场景 |
| 锁粒度 | 行锁(by undo_log) | 业务自定义(可更细) |
| 数据库要求 | 必须支持本地事务 | 不限 |
| 开发成本 | 低 | 中等(需处理幂等/空回滚/悬挂) |
推荐策略:
- 业务初期、并发不高:优先 AT 模式,快速落地
- 核心支付、资金流水等高并发场景:使用 TCC 模式,保障性能和一致性
八、总结
本文完整演示了 Spring Boot 3.x + Seata 2.1.0 的集成过程:
- AT 模式:零侵入,通过
@GlobalTransactional即可开启全局事务,适合大多数业务场景 - TCC 模式:需要实现 Try/Confirm/Cancel 三个阶段,但性能更好,适合高并发金融场景
- 踩坑总结:重点关注 vgroup-mapping 配置、DataSourceProxy 冲突、TCC 幂等/空回滚/悬挂问题
分布式事务本质上是在 数据一致性 和 系统可用性/性能 之间做权衡。Seata 提供了一套完善的解决方案,但落地时仍需结合具体业务场景选择合适的模式。
参考资料
如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区交流~
评论区