SpringCloud Alibaba 核心组件解析:分布式事务(Seata)

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-service2002seata-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 回滚的基础
相关推荐
于指尖飞舞1 小时前
java后端面试题(jvm极简)
java·开发语言·jvm
Seven971 小时前
面试官:你们项目里的线程池是怎么用的?怎么管理的?
java
xieliyu.1 小时前
Java数据结构:从0开始手搓Hash桶
java·数据结构·哈希算法
影视飓风TIM1 小时前
C++ 核心语法笔记:拷贝构造、深浅拷贝与运算符重载
java·开发语言·javascript
ACP广源盛139246256731 小时前
GSV6155@ACP#DP 1.4a 重定时器芯片,物理 AI 信号长距传输的稳定保障
大数据·人工智能·分布式·嵌入式硬件·spark
极创信息1 小时前
信创产品适配测试认证,域名和SSL是必须的吗?
java·开发语言·网络·python·网络协议·ruby·ssl
Y学院2 小时前
Java 智能体开发实战:从核心架构到生产级落地,告别AI调用积木式编程
java·人工智能·架构
Javatutouhouduan2 小时前
2026年Java面试核心讲(终极版)全网首次开源!
java·jvm·java多线程·java面试·后端开发·java程序员·java八股文
摇滚侠2 小时前
MyBatis 入门到项目实战 MyBatis 各种查询功能 30-33
java·后端·spring·maven·intellij-idea·mybatis