Spring Cloud Alibaba微服务实战:Seata实现订单-库存-账户分布式事务一致性

在电商系统的典型下单场景中,用户支付成功后需要同时完成订单创建、库存扣减和账户余额变更三个操作。这三个操作分别属于不同的微服务,部署在不同的服务器上,使用独立的数据库。如何保证这三个操作要么全部成功,要么全部失败?这就是分布式事务要解决的核心问题。

传统单机事务的ACID特性在分布式环境下不再适用,我们需要引入专门的分布式事务解决方案。Seata作为Spring Cloud Alibaba生态中的重要组件,提供了高性能且易用的分布式事务支持。本文将基于一个真实的电商下单案例,深入讲解如何使用Seata的AT模式实现跨服务的事务一致性。

1. Seata核心概念与架构设计

1.1 Seata的三种事务模式

Seata支持三种分布式事务模式:

  • AT模式(Auto Transaction):自动补偿型事务,基于两阶段提交实现。业务代码无需手动编写回滚逻辑,通过解析SQL自动生成反向操作。
  • TCC模式:需要业务代码实现Try、Confirm、Cancel三个接口,适合对一致性要求极高的场景。
  • SAGA模式:长事务解决方案,通过状态机驱动业务流程,适合业务流程长且需要最终一致性的场景。

对于大多数业务场景,AT模式已经能够满足需求,且对代码侵入性最小。本文案例将采用AT模式实现。

1.2 Seata AT模式的核心组件

Seata的架构包含三个核心组件:

组件名称作用
Transaction Coordinator (TC)事务协调器,维护全局事务的运行状态,负责协调分支事务的提交或回滚
Transaction Manager (TM)定义全局事务边界,负责开启、提交或回滚全局事务
Resource Manager (RM)管理分支事务处理的资源,负责分支事务注册、状态汇报和本地事务提交/回滚

在电商案例中:

  • 业务服务作为TM,使用@GlobalTransactional注解开启全局事务
  • 订单、库存、账户服务作为RM,处理各自的分支事务
  • Seata Server作为TC,协调所有事务参与者的行为

1.3 undo_log表的工作原理

AT模式的核心机制依赖于undo_log表,其关键字段如下:

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

当RM执行业务SQL时,Seata会:

  1. 前置镜像查询:记录数据修改前的状态
  2. 执行业务SQL
  3. 后置镜像查询:记录数据修改后的状态
  4. 将前后镜像写入undo_log

如果事务需要回滚,Seata会根据undo_log中的记录生成反向SQL并执行,将数据恢复到事务开始前的状态。

2. 环境准备与Seata Server部署

2.1 基础环境要求

  • JDK 1.8(Seata目前对高版本JDK支持不完善)
  • MySQL 5.7+(案例使用MySQL 8.0)
  • Nacos 1.4+ 作为配置中心和注册中心
  • Spring Boot 2.3.x
  • Spring Cloud Hoxton.SR12
  • Spring Cloud Alibaba 2.2.6.RELEASE

2.2 Seata Server安装配置

  1. 从官网下载Seata Server(本文使用1.4.2版本):

    wget https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.tar.gz
    tar -xzvf seata-server-1.4.2.tar.gz
    cd seata
    
  2. 初始化数据库表:

    • 创建seata数据库,执行script/server/db/mysql.sql
    • 在每个业务数据库中添加undo_log表(执行script/client/at/db/mysql.sql
  3. 修改conf/registry.conf,配置使用Nacos作为注册中心:

    registry {
      type = "nacos"
      nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        namespace = ""
        cluster = "default"
      }
    }
    
  4. 启动Seata Server:

    sh bin/seata-server.sh -p 8091 -m file
    

提示:生产环境建议使用db模式存储事务日志(-m db),并配置集群部署

3. 电商案例实现

3.1 系统架构设计

案例包含四个微服务:

  1. business-service:业务入口,处理用户下单请求
  2. order-service:创建订单记录
  3. storage-service:扣减商品库存
  4. account-service:扣减用户账户余额

服务调用关系如下:

business-service → order-service → account-service
business-service → storage-service

3.2 关键代码实现

  1. 在business-service中定义全局事务:

    @RestController
    public class BusinessController {
        
        @Autowired
        private OrderService orderService;
        
        @Autowired
        private StorageService storageService;
        
        @GlobalTransactional(timeoutMills = 300000, name = "create-order-tx")
        @PostMapping("/placeOrder")
        public String placeOrder(@RequestBody OrderRequest request) {
            // 扣减库存
            storageService.deduct(request.getCommodityCode(), request.getCount());
            // 创建订单
            orderService.create(request.getUserId(), request.getCommodityCode(), request.getCount());
            return "success";
        }
    }
    
  2. order-service创建订单后调用account-service:

    @Service
    public class OrderServiceImpl implements OrderService {
        
        @Autowired
        private AccountService accountService;
        
        @Transactional
        public void create(String userId, String commodityCode, int count) {
            // 创建订单逻辑
            Order order = new Order();
            order.setUserId(userId);
            order.setCommodityCode(commodityCode);
            order.setCount(count);
            order.setMoney(calculateAmount(commodityCode, count));
            orderMapper.insert(order);
            
            // 扣减账户余额
            accountService.debit(userId, order.getMoney());
        }
    }
    
  3. account-service中模拟异常触发回滚:

    @Service
    public class AccountServiceImpl implements AccountService {
        
        @Transactional
        public void debit(String userId, BigDecimal money) {
            // 模拟用户U002触发异常
            if ("U002".equals(userId)) {
                throw new RuntimeException("模拟异常触发回滚");
            }
            
            accountMapper.updateBalance(userId, money.negate());
        }
    }
    

3.3 客户端配置

每个微服务需要添加以下配置:

  1. 引入依赖:

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    
  2. 配置application.yml:

    seata:
      application-id: ${spring.application.name}
      tx-service-group: my_test_tx_group
      service:
        vgroup-mapping:
          my_test_tx_group: default
      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
    
  3. 配置数据源代理:

    @Configuration
    public class DataSourceConfig {
        
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DruidDataSource druidDataSource() {
            return new DruidDataSource();
        }
        
        @Primary
        @Bean("dataSource")
        public DataSource dataSource(DruidDataSource druidDataSource) {
            return new DataSourceProxy(druidDataSource);
        }
    }
    

4. 事务执行过程深度解析

4.1 正常流程事务执行时序

  1. TM(business-service)向TC申请开启全局事务,TC生成全局事务ID(XID)
  2. TM将XID通过Feign调用传递到各个RM服务
  3. 每个RM执行本地事务前,先向TC注册分支事务
  4. RM执行本地事务,生成undo_log记录
  5. 所有分支事务执行成功后,TM通知TC提交全局事务
  6. TC异步通知各RM删除undo_log记录

4.2 异常回滚流程

当account-service抛出异常时:

  1. 异常通过Feign调用链回传到TM
  2. TM捕获异常后通知TC回滚全局事务
  3. TC查询该全局事务下的所有分支事务
  4. TC通知各RM执行回滚操作
  5. RM根据undo_log中的记录生成反向SQL并执行
  6. RM删除undo_log记录,回滚完成

4.3 XID传递机制

Seata通过以下方式传递XID:

  1. 服务间调用:通过Feign的RequestInterceptor在header中添加TX_XID

    public class SeataFeignInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate template) {
            String xid = RootContext.getXID();
            if (StringUtils.isNotBlank(xid)) {
                template.header(RootContext.KEY_XID, xid);
            }
        }
    }
    
  2. 线程间传递:通过RootContext绑定到当前线程

    // 设置XID
    RootContext.bind(xid);
    // 获取XID
    String xid = RootContext.getXID();
    
  3. 跨线程池传递:需要手动传递XID,或使用Seata的@GlobalTransactional扩展点

4.4 性能优化建议

  1. undo_log表优化

    • 定期清理已完成的undo_log记录
    • 对大字段使用压缩存储
    • 添加合适的索引
  2. Seata Server配置调优

    store {
      mode = "db"
      db {
        datasource = "druid"
        dbType = "mysql"
        driverClassName = "com.mysql.cj.jdbc.Driver"
        url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true"
        user = "seata"
        password = "seata"
        minConn = 5
        maxConn = 30
        maxWait = 5000
      }
    }
    
  3. 事务分组隔离

    • 不同业务使用不同的事务分组
    • 配置不同的Seata集群处理不同分组的事务

5. 生产环境最佳实践

5.1 高可用部署方案

  1. Seata Server集群

    • 部署至少3个节点
    • 使用Nacos或其它注册中心实现服务发现
    • 配置相同的seata.registry.cluster名称
  2. 数据库高可用

    • 使用MySQL主从复制
    • 配置读写分离
    • 定期备份事务日志
  3. 客户端配置建议

    seata:
      enable-auto-data-source-proxy: false # 手动配置数据源代理
      disable-global-transaction: false
      client:
        rm:
          report-retry-count: 5
          table-meta-check-enable: false
          report-success-enable: false
        tm:
          commit-retry-count: 5
          rollback-retry-count: 5
    

5.2 常见问题排查

  1. XID未传递

    • 检查Feign拦截器是否配置正确
    • 确认服务间调用是否跳过了Feign
    • 检查日志中是否有can not found global transaction xid警告
  2. 分支事务未注册

    • 确认RM是否配置了DataSourceProxy
    • 检查Seata Server日志是否有注册请求
    • 确认tx-service-group配置是否正确
  3. undo_log不生效

    • 检查业务库中是否有undo_log
    • 确认SQL是否被Seata解析(仅支持DML语句)
    • 检查auto-commit是否为false

5.3 监控与告警

  1. Seata Server监控指标

    • 全局事务数量
    • 分支事务数量
    • 事务成功率
    • 平均处理时间
  2. Prometheus配置示例

    metrics:
      enabled: true
      registry-type: compact
      exporter:
        prometheus:
          enabled: true
          port: 9898
    
  3. 关键告警项

    • 事务失败率超过阈值
    • 事务处理时间过长
    • Seata Server节点不可用

在实际项目中,我们发现当业务高峰期事务量激增时,适当调整seata.server.session.enable-branch-async-remove参数可以显著提升性能。另外,对于非核心业务,可以考虑采用SAGA模式实现最终一致性,减轻系统压力。

更多推荐