Seata分布式事务实战:AT模式在Spring Boot微服务中的集成配置与排障

微服务架构下跨服务的数据一致性是核心工程挑战。Seata的AT(Automatic Transaction)模式通过SQL解析和undo_log自动回滚机制,对业务代码侵入性最低,只需加@GlobalTransactional注解即可实现跨库事务。本文围绕Seata 2.0在Spring Boot 3.x微服务中的集成部署,拆解事务协调器配置、全局事务传播和常见故障排查。

Seata Server部署与存储模式配置

Seata Server(TC,Transaction Coordinator)是事务协调器,负责维护全局事务和分支事务的状态。生产环境使用Nacos作为注册中心,MySQL作为存储模式:

# application.yml(Seata Server)
seata:
  server:
    service-port: 8091
    max-commit-retry-timeout: 100
    max-rollback-retry-timeout: 100
    rollback-retry-timeout-unlock-enable: false
    enable-check-auth: true
    retry_dead-threshold: 130000
  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      url: jdbc:mysql://mysql.internal:3306/seata?useUnicode=true&characterEncoding=utf8
      user: seata
      password: ${SEATA_DB_PASSWORD}
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: nacos.internal:8848
      namespace: seata
      group: SEATA_GROUP
      cluster: default

Seata 2.0新增Raft集群模式,不再依赖外部数据库存储事务日志,适合不想引入额外MySQL依赖的场景。但Raft模式要求至少3节点部署,延迟比DB模式高10-15ms。存储选型:已有MySQL基础设施用DB模式,云原生环境用Raft模式。

初始化Seata数据库表:

-- global_table: 全局事务表
CREATE TABLE IF NOT EXISTS `global_table` (
  `xid` VARCHAR(128) NOT NULL,
  `transaction_id` BIGINT,
  `status` TINYINT NOT NULL,
  `application_id` VARCHAR(32),
  `transaction_service_group` VARCHAR(32),
  `transaction_name` VARCHAR(128),
  `timeout` INT,
  `begin_time` BIGINT,
  `application_data` VARCHAR(2000),
  `gmt_create` DATETIME,
  `gmt_modified` DATETIME,
  PRIMARY KEY (`xid`),
  KEY `idx_status_gmt_modified` (`status`,`gmt_modified`)
) ENGINE=InnoDB;

-- branch_table: 分支事务表
-- lock_table: 全局锁表
-- distributed_lock: 分布式锁表

Spring Boot微服务客户端集成

订单服务和库存服务都需要集成Seata Client。以Maven依赖和配置为例:

<!-- pom.xml -->
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>2.0.0</version>
</dependency>

<!-- 数据源代理 -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.2.20</version>
</dependency>
# application.yml(微服务端)
seata:
  enabled: true
  application-id: order-service
  tx-service-group: yunthe_tx_group
  service:
    vgroup-mapping:
      yunthe_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: nacos.internal:8848
      namespace: seata
      group: SEATA_GROUP
  data-source-proxy-mode: AT
  client:
    rm:
      report-success-enable: true
      table-meta-check-enable: false
      async-commit-buffer-limit: 10000
      lock-retry-times: 30
      lock-retry-interval: 10
    tm:
      commit-retry-count: 5
      rollback-retry-count: 5
      default-global-transaction-timeout: 60000

每个参与全局事务的数据库都需要创建undo_log表:

-- 在业务数据库中创建undo_log表
CREATE TABLE IF NOT EXISTS `undo_log` (
  `branch_id` BIGINT NOT NULL COMMENT '分支事务ID',
  `xid` VARCHAR(128) NOT NULL COMMENT '全局事务ID',
  `context` VARCHAR(128) NOT NULL COMMENT '上下文',
  `rollback_info` LONGBLOB NOT NULL COMMENT '回滚信息',
  `log_status` INT NOT NULL COMMENT '状态',
  `log_created` DATETIME NOT NULL COMMENT '创建时间',
  `log_modified` DATETIME NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`branch_id`),
  KEY `idx_log_created` (`log_created`)
) ENGINE=InnoDB COMMENT='AT模式事务日志表';

全局事务注解与业务代码实现

订单服务作为事务发起方,使用@GlobalTransactional注解开启全局事务:

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryFeignClient inventoryClient;
    
    @Autowired
    private AccountFeignClient accountClient;

    @GlobalTransactional(name = "createOrder", timeoutMills = 60000, rollbackFor = Exception.class)
    public OrderResult createOrder(OrderRequest request) {
        // 1. 创建订单(本地事务,自动注册为分支事务)
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setAmount(request.getAmount());
        order.setStatus("CREATED");
        orderMapper.insert(order);
        
        // 2. 扣减库存(远程调用,通过Feign传播xid)
        inventoryClient.deduct(request.getProductId(), request.getQuantity());
        
        // 3. 扣减账户余额
        accountClient.debit(request.getUserId(), request.getAmount());
        
        // 4. 更新订单状态
        order.setStatus("PAID");
        orderMapper.updateStatus(order);
        
        return new OrderResult(order.getId(), "SUCCESS");
    }
}

Feign拦截器自动传播全局事务ID(xid),下游服务无需额外配置。Seata通过SeataFeignClient拦截请求头中的TX_XID字段,将xid绑定到当前线程的RootContext:

// 库存服务——分支事务自动加入全局事务
@Service
public class InventoryService {

    @Autowired
    private InventoryMapper inventoryMapper;

    @Transactional(rollbackFor = Exception.class)
    public void deduct(Long productId, Integer quantity) {
        Inventory inventory = inventoryMapper.selectByProductId(productId);
        if (inventory.getStock() < quantity) {
            throw new BusinessException("库存不足");
        }
        inventoryMapper.deductStock(productId, quantity);
        // Seata AT模式自动记录undo_log
        // 全局回滚时根据undo_log自动恢复库存
    }
}

AT模式工作原理与undo_log机制

AT模式分为两阶段:一阶段执行业务SQL并记录undo_log,二阶段根据全局事务状态提交或回滚。一阶段流程:

1. Seata拦截业务SQL,执行前查询并缓存前镜像(before image)。

2. 执行业务SQL,查询后镜像(after image)。

3. 将前后镜像和SQL类型组装成undo_log插入undo_log表。

4. 获取全局锁(lock_table),提交本地事务。

二阶段提交:异步删除undo_log记录,释放全局锁。二阶段回滚:根据undo_log的前镜像生成反向SQL恢复数据,释放全局锁。

// undo_log中存储的回滚信息JSON结构
{
  "branchId": 1234567890,
  "sqlUndoLogs": [{
    "sqlType": "UPDATE",
    "tableName": "inventory",
    "beforeImage": {
      "rows": [{
        "fields": [
          {"name": "id", "value": 1},
          {"name": "stock", "value": 100}
        ]
      }]
    },
    "afterImage": {
      "rows": [{
        "fields": [
          {"name": "id", "value": 1},
          {"name": "stock", "value": 95}
        ]
      }]
    }
  }]
}

常见故障排查与解决方案

故障一:全局事务超时回滚。默认超时60秒,如果业务逻辑执行时间较长(如等待外部接口响应),超时后TC强制回滚。调整timeoutMills参数,或优化业务逻辑减少事务持有时间。原则:全局事务内不要包含RPC长等待操作。

故障二:undo_log表不存在导致分支事务注册失败。错误日志Table 'business_db.undo_log' doesn't exist。确保每个参与全局事务的数据库都创建了undo_log表。

故障三:全局锁冲突。两个全局事务同时修改同一行数据,后到的事务获取全局锁超时。检查lock_table中的table_namepk字段定位冲突行。业务层建议避免热点数据并发修改,或使用乐观锁减少锁竞争。

// 全局锁冲突排查
// 查看lock_table中的锁记录
SELECT * FROM seata.lock_table WHERE table_name = 'inventory' AND row_key LIKE '%1:%';

// 查看全局事务状态
SELECT xid, status, transaction_name, gmt_create 
FROM seata.global_table 
WHERE status IN (0, 1)  -- 0=Begin, 1=Committing
ORDER BY gmt_create DESC;

// 手动清理悬挂事务(谨慎操作)
DELETE FROM seata.global_table WHERE status = 4 AND gmt_modified < DATE_SUB(NOW(), INTERVAL 1 HOUR);
DELETE FROM seata.branch_table WHERE xid NOT IN (SELECT xid FROM seata.global_table);

故障四:xid未传播到下游服务。检查Feign拦截器是否正确注册,Seata 2.0自动配置SeataFeignClient。如果使用RestTemplate或WebClient,需要手动添加SeataRequestInterceptor。Dubbo场景需要配置seata-spring-boot-starter的Dubbo过滤器。

生产环境建议配合Seata控制台监控全局事务执行情况,关注事务成功率、平均耗时和回滚率三个指标。正常水平下回滚率应低于0.1%,如果回滚率突然升高,优先排查全局锁冲突和事务超时问题。

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

(0)
小编小编
上一篇 3小时前
下一篇 3小时前

相关推荐