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:先暴露痛点
// ❌ 没有分布式事务时:本地事务管不了远程调用@Transactional// 只管当前数据库publicvoidcreateOrder(Orderorder){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
# 下载 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.bat3.3.3 步骤 ②:三个服务公共 Seata 配置
# 每个服务的 application.yml(订单/库存/账户 都相同)seata:registry:type:nacosnacos:server-addr:127.0.0.1:8848namespace:""group:SEATA_GROUPapplication:seata-servertx-service-group:default_tx_group# 事务分组service:vgroup-mapping:# 事务分组 → TC 集群映射default_tx_group:defaultdata-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(核心)
// seata-order-service2001/.../service/impl/OrderServiceImpl.java@Service@Slf4jpublicclassOrderServiceImplextendsServiceImpl<OrderMapper,Order>implementsOrderService{@ResourceprivateOrderMapperorderMapper;@ResourceprivateStorageFeignApistorageFeignApi;// Feign → 库存服务@ResourceprivateAccountFeignApiaccountFeignApi;// Feign → 账户服务@Override@GlobalTransactional(name="zzyy-create-order",rollbackFor=Exception.class)// ↑ ↑ ↑ 核心!声明全局事务publicvoidcreate(Orderorder){// 查看全局事务 XID(Seata 自动生成)Stringxid=RootContext.getXID();log.info("==================>开始新建订单, xid: {}",xid);// ① 新建订单order.setStatus(0);intresult=orderMapper.insert(order);OrderorderFromDB=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 接口
// cloud-api-commons/.../apis/StorageFeignApi.java@FeignClient(name="seata-storage-service")publicinterfaceStorageFeignApi{@PostMapping("/storage/decrease")ResultDatadecrease(@RequestParam("productId")LongproductId,@RequestParam("count")Integercount);}// cloud-api-commons/.../apis/AccountFeignApi.java@FeignClient(value="seata-account-service")publicinterfaceAccountFeignApi{@PostMapping("/account/decrease")ResultDatadecrease(@RequestParam("userId")LonguserId,@RequestParam("money")Longmoney);}3.3.6 步骤 ⑤:库存服务 — RM
// seata-storage-service2002/.../service/impl/StorageServiceImpl.java@Service@Slf4jpublicclassStorageServiceImplextendsServiceImpl<StorageMapper,Storage>implementsStorageService{@ResourceprivateStorageMapperstorageMapper;@Overridepublicvoiddecrease(LongproductId,Integercount){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(含超时测试)
// seata-account-service2003/.../service/impl/AccountServiceImpl.java@Service@Slf4jpublicclassAccountServiceImplextendsServiceImpl<AccountMapper,Account>implementsAccountService{@ResourceAccountMapperaccountMapper;@Overridepublicvoiddecrease(LonguserId,Longmoney){log.info("------->account-service 扣减余额开始");accountMapper.decrease(userId,money);// 模拟超时异常 → 触发全局事务回滚myTimeOut();// int age = 10/0; // 也可模拟除零异常log.info("------->account-service 扣减余额结束");}privatestaticvoidmyTimeOut(){try{TimeUnit.SECONDS.sleep(65);}catch(InterruptedExceptione){e.printStackTrace();}}}💡测试:调用
http://localhost:2001/order/create?...→ 账户服务 sleep 65 秒 → TC 超时 → 全局回滚:订单删除、库存恢复、余额恢复。
3.4 深入原理 — undo_log 表结构
每张参与分布式事务的表都需要创建undo_log表:
CREATETABLE`undo_log`(`id`bigintNOTNULLAUTO_INCREMENT,`branch_id`bigintNOTNULLCOMMENT'分支事务ID',`xid`varchar(100)NOTNULLCOMMENT'全局事务ID',`context`varchar(128)NOTNULL,`rollback_info`longblobNOTNULLCOMMENT'回滚信息(反向SQL)',`log_status`intNOTNULLCOMMENT'状态:0正常,1已全局提交',`log_created`datetimeNOTNULL,`log_modified`datetimeNOTNULL,PRIMARYKEY(`id`),UNIQUEKEY`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 +>
|