Seata AT模式分布式事务实战:Spring Boot微服务一致性方案与踩坑记录

微服务场景下为什么需要分布式事务

单体应用拆分为微服务后,一个业务操作往往涉及多个服务的数据写入。订单创建需要同时写订单库、扣减库存、扣减账户余额,任何一步失败都会导致数据不一致。本地事务无法跨服务,消息最终一致性方案在核心链路上又过于复杂。Seata的AT模式通过拦截SQL自动生成回滚日志,用最小的代码侵入解决分布式事务问题,是目前Spring Boot微服务体系中落地成本最低的方案。

Seata AT模式工作原理

AT模式的核心流程分为两个阶段:

一阶段:拦截业务SQL,在执行前保存数据快照(before image),执行SQL后保存数据快照(after image),生成回滚日志(undo log)。一阶段结束时本地事务提交,数据对其他事务可见。

二阶段:事务协调者根据各分支事务的执行结果决定提交或回滚。提交时异步清理undo log,回滚时根据undo log反向补偿数据。

Seata Server部署

使用Docker部署Seata Server:

version: '3'
services:
  seata-server:
    image: seataio/seata-server:2.2
    ports:
      - "8091:8091"
      - "7091:7091"
    environment:
      - SEATA_IP=192.168.1.100
    volumes:
      - ./seata-config:/seata-server/resources

Seata Server配置(application.yml):

server:
  port: 7091

seata:
  config:
    type: nacos
    nacos:
      server-addr: 192.168.1.100:8848
      namespace: seata
      group: SEATA_GROUP
  registry:
    type: nacos
    nacos:
      server-addr: 192.168.1.100:8848
      namespace: seata
      group: SEATA_GROUP
  store:
    mode: db
    db:
      datasource: druid
      url: jdbc:mysql://192.168.1.100:3306/seata?useSSL=false
      user: seata
      password: seata_pwd

Spring Boot微服务接入Seata

添加依赖

<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>2.2.0</version>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  <version>2023.0.1.2</version>
</dependency>

数据源代理配置

@Configuration
public class DataSourceProxyConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new HikariDataSource();
    }

    @Bean
    @Primary
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
}

Seata通过DataSourceProxy拦截所有SQL执行,自动管理undo log。必须确保业务数据源被代理,否则事务回滚失效。

undo_log表创建

每个参与分布式事务的数据库都必须创建undo_log表:

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',
  `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
  `log_status`    INT          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 COMMENT = 'AT transaction mode undo log';

业务代码实战:订单-库存-账户

订单服务(TM端,事务发起方)

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockClient stockClient;
    @Autowired
    private AccountClient accountClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public Order createOrder(OrderDTO dto) {
        // 1. 创建订单
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setProductId(dto.getProductId());
        order.setCount(dto.getCount());
        order.setAmount(dto.getAmount());
        order.setStatus("INIT");
        orderMapper.insert(order);

        // 2. 扣减库存
        stockClient.deduct(dto.getProductId(), dto.getCount());

        // 3. 扣减账户余额
        accountClient.debit(dto.getUserId(), dto.getAmount());

        // 4. 更新订单状态
        order.setStatus("SUCCESS");
        orderMapper.updateById(order);

        return order;
    }
}

@GlobalTransactional注解标记全局事务边界。当stockClient或accountClient调用失败抛出异常时,Seata自动回滚所有分支事务。

库存服务(RM端,事务分支)

@Service
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    public void deduct(String productId, int count) {
        Stock stock = stockMapper.selectByProductId(productId);
        if (stock == null || stock.getRemain() < count) {
            throw new BusinessException("库存不足");
        }
        stock.setRemain(stock.getRemain() - count);
        stockMapper.updateById(stock);
    }
}

RM端不需要加@GlobalTransactional注解。Seata通过DataSourceProxy自动拦截SQL,在一阶段保存undo log。回滚时根据undo log恢复原始数据。

踩坑记录与解决方案

坑1:数据源代理不生效

Spring Boot自动配置的DataSource如果优先级高于代理配置,会导致SQL不被拦截。解决方式:排除Seata的自动配置,手动配置代理:

@SpringBootApplication(exclude = {
    SeataDataSourceAutoConfiguration.class
})
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

坑2:undo_log序列化失败

当表中存在JSON、BLOB等特殊类型字段时,Jackson序列化undo_log可能失败。配置Kryo序列化:

seata:
  client:
    undo:
      data-serialization: kryo

坑3:全局事务超时

默认超时60秒,涉及多个远程调用的长链路事务容易超时。调整超时时间:

@GlobalTransactional(
    name = "create-order",
    rollbackFor = Exception.class,
    timeoutMills = 300000
)

坑4:读隔离问题

AT模式一阶段提交后数据对其他事务可见,可能读到中间状态数据。解决方案:在Seata配置中开启全局锁:

seata:
  client:
    undo:
      log-serialization: kryo
      only-care-update-columns: true
    lock:
      retry-times: 30
      retry-interval: 10

全局锁确保一阶段提交后、二阶段提交前,其他事务无法修改同一行数据,避免脏读。

生产环境监控与性能调优

Seata内置指标暴露,对接Prometheus:

seata:
  metrics:
    enabled: true
    exporter-type: prometheus
    exporter-prometheus-port: 9898

关键监控指标:seata.transaction.active(活跃事务数)、seata.transaction.commit.rate(提交成功率)、seata.transaction.rollback.rate(回滚率)。当回滚率超过10%时触发告警,排查是否有业务逻辑异常或网络超时。

性能调优建议

- 将Seata Server的存储模式从file改为db,支持集群部署
- undo_log表数据量增长后需要定期清理,建议设置定时任务删除7天前的记录
- 远程调用使用OpenFeign时配置超时和重试,避免因网络抖动导致全局事务回滚
- 高并发场景下将Seata Server从单节点扩展为3节点集群,通过Nacos做服务发现

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/seataat-mo-shi-fen-bu-shi-shi-wu-shi-zhan-springboot-wei-fu/

(0)
小编小编
上一篇 26分钟前
下一篇 26分钟前

相关推荐