SpringCloud Alibaba 核心组件解析:分布式事务(Seata)
技术栈 :Spring Boot 3.2.0 + Spring Cloud Alibaba 2023.0.0.0-RC1 + Seata AT 模式 + Nacos + MyBatis-Plus
3.1 是什么 --- 分布式事务的核心概念
3.1.1 生活化类比:跨国汇款
你在中国的银行 A 向美国的银行 B 汇款 1000 美元:
① 银行 A 从你的账户扣除 $1000
② 银行 A 通知银行 B:"给那个账户加 $1000"
③ 银行 B 收到通知,给目标账户增加 $1000
问题:如果第③步失败了(网络中断、银行 B 系统故障),
第①步已经扣了你的钱,怎么办?
→ 需要"分布式事务"来回滚第①步的操作。
3.1.2 技术定义
分布式事务:一个业务操作跨越多个独立的数据库/服务,需要保证所有操作要么全部成功(Commit),要么全部失败回滚(Rollback)。
3.1.3 Seata 的角色分工
┌──────────────────────┐
│ TC (Transaction │ ← 事务协调者(独立部署的 seata-server)
│ Coordinator) │ 维护全局事务的提交/回滚状态
└──────────┬───────────┘
│ 注册/汇报
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌─────────┐┌─────────┐┌─────────┐
│ TM ││ RM ││ RM │
│(发起方) ││(参与方) ││(参与方) │
└─────────┘└─────────┘└─────────┘
| 角色 | 说明 | 对应本项目的服务 |
|---|---|---|
| TC | 事务协调者,独立部署 | seata-server |
| TM | 事务管理器,标注 @GlobalTransactional |
seata-order-service2001 |
| RM | 资源管理器,管理分支事务 | seata-storage-service2002、seata-account-service2003 |
3.2 为什么 --- 四种分布式事务模式对比
3.2.1 AT 模式(本项目使用,推荐)
原理:一阶段执行业务 SQL + 记录 undo_log;二阶段提交时删除 undo_log,回滚时执行反向 SQL。
| 优点 | 缺点 |
|---|---|
| ✅ 对业务代码零侵入 | ❌ 依赖数据库 ACID |
| ✅ 自动生成回滚 SQL | ❌ 仅支持关系型数据库 |
| ✅ 性能好(一阶段即提交) | ❌ 需要额外的 undo_log 表 |
3.2.2 四种模式对比
| 模式 | 数据一致性 | 性能 | 业务侵入 | 适用场景 |
|---|---|---|---|---|
| AT | 最终一致 | ⭐⭐⭐⭐ | 无 | 一般微服务(推荐) |
| TCC | 最终一致 | ⭐⭐⭐⭐⭐ | 高(需实现 try/confirm/cancel) | 高性能场景 |
| SAGA | 最终一致 | ⭐⭐⭐⭐⭐ | 中(需实现补偿) | 长事务/老系统 |
| XA | 强一致 | ⭐⭐ | 无 | 银行/金融 |
3.2.3 AT 模式回滚原理
一阶段(执行业务 SQL + 记录 undo_log):
Order → INSERT INTO t_order (id=1, status=0)
undo_log: DELETE FROM t_order WHERE id=1
Storage → UPDATE t_storage SET used=used+10, residue=residue-10 WHERE product_id=1
undo_log: UPDATE t_storage SET used=used-10, residue=residue+10 WHERE product_id=1
Account → UPDATE t_account SET used=used+100, residue=residue-100 WHERE user_id=1
undo_log: UPDATE t_account SET used=used-100, residue=residue+100 WHERE user_id=1
二阶段-提交(无异常):删除所有 undo_log
二阶段-回滚(异常触发):执行 undo_log 中的反向 SQL,数据恢复到一阶段前
3.3 怎么做 --- Seata AT 完整实战
3.3.0 小 Demo:先暴露痛点
java
// ❌ 没有分布式事务时:本地事务管不了远程调用
@Transactional // 只管当前数据库
public void createOrder(Order order) {
orderMapper.insert(order); // ✅ 这个能回滚
storageFeignApi.decrease(...); // ❌ Feign 调用不受 @Transactional 控制
accountFeignApi.decrease(...); // ❌ 万一这里失败,库存已经扣了
}
3.3.1 项目架构
用户下单 (userId=1, productId=1, count=10, money=100)
│
▼
┌─────────────────────────┐
│ seata-order-service2001 │ TM(发起方)
│ 数据库: seata_order │ → ① 创建订单
│ @GlobalTransactional │
└──────┬──────────┬────────┘
│ Feign │ Feign
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ storage-2002 │ │ account-2003 │
│ seata_storage│ │ seata_account │
│ → ② 扣库存 │ │ → ③ 扣余额 │
└──────────────┘ └──────────────────┘
3.3.2 步骤 ①:启动 Seata Server
bash
# 下载 seata-server,修改 conf/application.yml 注册到 Nacos
# seata:
# registry:
# type: nacos
# nacos:
# server-addr: 127.0.0.1:8848
# group: SEATA_GROUP
# application: seata-server
# Windows 启动
seata-server.bat
3.3.3 步骤 ②:三个服务公共 Seata 配置
yaml
# 每个服务的 application.yml(订单/库存/账户 都相同)
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务分组
service:
vgroup-mapping: # 事务分组 → TC 集群映射
default_tx_group: default
data-source-proxy-mode: AT # AT 模式
logging:
level:
io.seata: info
配置四要素:
| 配置 | 含义 |
|---|---|
tx-service-group |
事务分组名,与 TC 端 service.vgroupMapping 对应 |
vgroup-mapping |
将事务分组映射到 TC 集群名(生产环境有多个 TC 集群) |
registry.type: nacos |
TM/RM 通过 Nacos 发现 TC 地址 |
data-source-proxy-mode: AT |
启用 AT 模式数据源代理 |
3.3.4 步骤 ③:订单服务 --- TM(核心)
java
// seata-order-service2001/.../service/impl/OrderServiceImpl.java
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private StorageFeignApi storageFeignApi; // Feign → 库存服务
@Resource
private AccountFeignApi accountFeignApi; // Feign → 账户服务
@Override
@GlobalTransactional(name = "zzyy-create-order", rollbackFor = Exception.class)
// ↑ ↑ ↑ 核心!声明全局事务
public void create(Order order) {
// 查看全局事务 XID(Seata 自动生成)
String xid = RootContext.getXID();
log.info("==================>开始新建订单, xid: {}", xid);
// ① 新建订单
order.setStatus(0);
int result = orderMapper.insert(order);
Order orderFromDB = null;
if (result > 0) {
orderFromDB = orderMapper.selectById(order.getId());
log.info("-------> 新建订单成功: {}", orderFromDB);
// ② 扣减库存(Feign 远程调用)
log.info("-------> 开始调用 Storage 扣减库存");
storageFeignApi.decrease(orderFromDB.getProductId(),
orderFromDB.getCount());
log.info("-------> 结束调用 Storage");
// ③ 扣减余额(Feign 远程调用)
log.info("-------> 开始调用 Account 扣减余额");
accountFeignApi.decrease(orderFromDB.getUserId(),
orderFromDB.getMoney());
log.info("-------> 结束调用 Account");
// ④ 修改订单状态 → 已完结
orderFromDB.setStatus(1);
orderMapper.updateById(orderFromDB);
log.info("-------> 修改订单状态完成");
}
log.info("==================>结束新建订单, xid: {}", xid);
}
}
3.3.5 步骤 ④:Feign 接口
java
// cloud-api-commons/.../apis/StorageFeignApi.java
@FeignClient(name = "seata-storage-service")
public interface StorageFeignApi {
@PostMapping("/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}
// cloud-api-commons/.../apis/AccountFeignApi.java
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi {
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId,
@RequestParam("money") Long money);
}
3.3.6 步骤 ⑤:库存服务 --- RM
java
// seata-storage-service2002/.../service/impl/StorageServiceImpl.java
@Service
@Slf4j
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage>
implements StorageService {
@Resource
private StorageMapper storageMapper;
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service 扣减库存开始");
storageMapper.decrease(productId, count);
log.info("------->storage-service 扣减库存结束");
}
}
// SQL: UPDATE t_storage SET used=used+#{count}, residue=residue-#{count}
// WHERE product_id=#{productId}
3.3.7 步骤 ⑥:账户服务 --- RM(含超时测试)
java
// seata-account-service2003/.../service/impl/AccountServiceImpl.java
@Service
@Slf4j
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account>
implements AccountService {
@Resource
AccountMapper accountMapper;
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service 扣减余额开始");
accountMapper.decrease(userId, money);
// 模拟超时异常 → 触发全局事务回滚
myTimeOut();
// int age = 10/0; // 也可模拟除零异常
log.info("------->account-service 扣减余额结束");
}
private static void myTimeOut() {
try { TimeUnit.SECONDS.sleep(65); }
catch (InterruptedException e) { e.printStackTrace(); }
}
}
💡 测试 :调用
http://localhost:2001/order/create?...→ 账户服务 sleep 65 秒 → TC 超时 → 全局回滚:订单删除、库存恢复、余额恢复。
3.4 深入原理 --- undo_log 表结构
每张参与分布式事务的表都需要创建 undo_log 表:
sql
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL COMMENT '分支事务ID',
`xid` varchar(100) NOT NULL COMMENT '全局事务ID',
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL COMMENT '回滚信息(反向SQL)',
`log_status` int NOT NULL COMMENT '状态:0正常,1已全局提交',
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB;
3.5 面试题
Q1:Seata 的 AT 模式和 TCC 模式有什么区别?什么时候用哪个?
答:
- AT:自动生成反向 SQL,业务代码零侵入,依赖数据库 ACID。适合一般业务场景。
- TCC:需手动实现 Try(预留)、Confirm(确认)、Cancel(取消)三个接口。性能更好但开发成本高。适合核心高性能链路。
- 选型原则:能用 AT 则 AT,除非有极致性能要求才用 TCC。
Q2:@GlobalTransactional 和 @Transactional 有什么区别?
答 :@Transactional 是 Spring 本地事务,只控制单个数据源;@GlobalTransactional 是 Seata 全局事务,由 TC 协调多个 RM 的本地事务,实现跨服务的整体提交或回滚。
Q3:Seata AT 模式下如果 TC Server 挂了怎么办?
答:TC 需要高可用集群部署。AT 模式下,一阶段本地事务已提交,业务不会阻塞;但全局回滚能力暂时丧失。TC 恢复后会根据 undo_log 继续处理未完成的全局事务。
3.6 踩坑指南
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 🔴 undo_log 不存在 | Table 'xxx.undo_log' doesn't exist |
未在每个业务库创建 undo_log 表 | 在每个参与分布式事务的数据库中执行建表 SQL |
| 🔴 全局事务不回滚 | 异常了数据还在 | rollbackFor = Exception.class 未配置 |
显式写 @GlobalTransactional(rollbackFor = Exception.class) |
| 🔴 TC 连接不上 | 启动报 can not connect to seata-server |
TC 未启动或 Nacos 配置不对 | 确认 seata-server.bat 已运行,Nacos 中可看到 seata-server 服务 |
| 🔴 MyBatis-Plus 版本冲突 | NoSuchMethodError |
Seata 对 MyBatis 版本敏感 | 统一使用父 POM 管理的版本 |
| 🔴 超时设置 | 默认超时太短 | TC 默认全局事务超时 60s | 在 TC 端 application.yml 中调大 service.default.grouplist.timeout |
3.7 章节总结
| 要点 | 说明 |
|---|---|
| 三大角色 | TC(协调者)+ TM(发起方,@GlobalTransactional)+ RM(参与方) |
| AT 模式 | 自动生成 undo_log 反向 SQL,业务零侵入,二阶段执行提交或回滚 |
| 四种模式 | AT(推荐)、TCC(高性能)、SAGA(长事务)、XA(强一致) |
| 核心注解 | @GlobalTransactional(name="...", rollbackFor=Exception.class) |
| 核心配置 | tx-service-group + vgroup-mapping + registry.type + data-source-proxy-mode |
| 每库必备 | undo_log 表,Seata 回滚的基础 |