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

分布式事务的核心问题与Seata的定位

微服务架构下,一次业务操作可能跨多个服务、操作多个数据库。本地事务无法保证跨服务的数据一致性。Seata是阿里开源的分布式事务解决方案,提供AT、TCC、Saga、XA四种模式。其中AT模式(自动补偿型)使用成本最低——业务代码无需感知分布式事务,Seata自动生成回滚SQL。

AT模式的工作机制:一阶段拦截业务SQL,在本地事务提交前记录修改前后的数据快照(before image / after image)到undo_log表,本地事务直接提交。二阶段如果全局事务成功,异步删除undo_log;如果失败,根据undo_log中的before image自动生成补偿SQL执行回滚。

Seata Server部署与配置

Seata架构中有三个角色:TC(事务协调器)、TM(事务管理器)、RM(资源管理器)。TC是独立部署的Server端,TM和RM是集成在微服务中的SDK。

使用Docker部署TC Server:

docker run -d --name seata-server \
  -p 8091:8091 \
  -p 7091:7091 \
  -e SEATA_IP=192.168.1.100 \
  -v /opt/seata/config:/seata-server/resources \
  seataio/seata-server:2.0.0

TC的存储模式选择——生产环境使用数据库模式(db模式)而非文件模式(file模式)。application.yml配置:

seata:
  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.1.100:3306/seata?useUnicode=true
      user: seata
      password: seata_password
  # 事务分组配置
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 192.168.1.100:8091

TC数据库需要创建三张表:global_table(全局事务表)、branch_table(分支事务表)、lock_table(全局锁表)。建表SQL在Seata官方仓库的script/server/db/mysql.sql中。

Spring Boot微服务集成Seata AT模式

每个参与分布式事务的微服务都需要集成Seata SDK。Maven依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2023.0.1.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

微服务的application.yml配置:

seata:
  enabled: true
  application-id: order-service
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 192.168.1.100:8091
  # AT模式配置
  data-source-proxy-mode: AT
  # 关闭数据源自动代理(手动指定需要代理的数据源)
  enable-auto-data-source-proxy: true

每个微服务的业务数据库需要创建undo_log表:

CREATE TABLE `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,such as serialization',
  `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 table';

全局事务的声明与传播

TM通过@GlobalTransactional注解开启全局事务,调用链路上的RM服务自动加入该全局事务:

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private AccountFeignClient accountFeignClient;
    @Autowired
    private StorageFeignClient storageFeignClient;
    
    @GlobalTransactional(timeoutMills = 60000, rollbackFor = Exception.class)
    public void createOrder(OrderDTO orderDTO) {
        // 1. 创建订单(本地事务,RM自动注册分支事务)
        Order order = new Order();
        order.setUserId(orderDTO.getUserId());
        order.setProductId(orderDTO.getProductId());
        order.setCount(orderDTO.getCount());
        order.setMoney(orderDTO.getMoney());
        order.setStatus(0);
        orderMapper.insert(order);
        
        // 2. 远程调用:扣减账户余额(RM自动注册分支事务)
        accountFeignClient.decrease(orderDTO.getUserId(), orderDTO.getMoney());
        
        // 3. 远程调用:扣减库存(RM自动注册分支事务)
        storageFeignClient.decrease(orderDTO.getProductId(), orderDTO.getCount());
        
        // 4. 修改订单状态
        order.setStatus(1);
        orderMapper.updateById(order);
        
        // 模拟异常,触发全局回滚
        if (orderDTO.getCount() > 100) {
            throw new RuntimeException("订单数量超过限制");
        }
    }
}

关键点:@GlobalTransactional只需要在事务发起方(TM)标注。下游服务(RM)的本地事务由Seata自动协调,不需要任何额外注解。全局事务ID(XID)通过HTTP Header或RPC上下文自动传播。

Feign调用链中的XID传播

Seata通过拦截器自动传播XID,但需要确保拦截器正确注册。OpenFeign场景下需要添加依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

如果XID没有正确传播,下游服务会以普通本地事务执行,全局回滚时无法补偿。验证方式——在下游服务中获取XID:

@RestController
public class AccountController {
    
    @PostMapping("/account/decrease")
    public String decrease(@RequestParam("userId") Long userId,
                           @RequestParam("money") BigDecimal money) {
        // 检查XID是否存在
        String xid = RootContext.getXID();
        if (xid == null) {
            log.warn("XID未传播,当前操作不在全局事务中");
        } else {
            log.info("当前全局事务XID: {}", xid);
        }
        
        accountService.decrease(userId, money);
        return "success";
    }
}

XID传播失败的常见原因:Seata的SeataFeignClient拦截器未生效(检查Spring Cloud Alibaba版本兼容性)、使用了异步线程池调用下游服务(XID存储在ThreadLocal中,跨线程需要手动传播)。

并发更新与全局锁机制

AT模式通过全局锁防止脏写——在本地事务提交前,Seata会向TC申请获取修改行的全局锁。如果另一个全局事务持有该行的锁,当前事务会等待(默认超时10秒)或抛出异常。

全局锁的获取发生在本地事务commit之前——这是AT模式与2PC的关键区别。本地事务一阶段就提交了(释放数据库锁),但全局锁持有到全局事务结束。这样设计是为了减少数据库锁的持有时间,提高并发性能。

脏写场景的回滚问题——如果一个全局事务回滚时,发现after image与数据库当前值不一致(说明被另一个事务修改了),Seata会抛出BranchRollbackFailed_Retriable异常,记录到TC的branch_table中,人工介入处理。

常见排障场景

全局事务无法回滚,分支事务数据未恢复——检查undo_log表是否存在且结构正确。Seata回滚依赖undo_log中的before image,如果undo_log被手动清除或表结构错误,回滚无法执行。查看TC Server日志中BranchRollbackFailed记录。

can not find undo_log异常——undo_log记录在分支事务commit后被删除,但全局回滚时找不到记录。常见于undo_log表与业务表不在同一个数据库。Seata要求undo_log和业务表在同一个datasource中。

死锁与锁超时——LockConflict异常表示全局锁竞争。排查是否存在多个全局事务频繁更新同一行数据。可以通过调整seata.client.rm.lock.retryInterval(锁重试间隔)和retryTimes(重试次数)来缓解。

服务治理层面的超时问题——@GlobalTransactionaltimeoutMills默认60秒。如果业务操作链路较长,超时后TC会主动触发回滚,但RPC调用可能仍在执行。确保timeout设置大于所有下游服务调用的最长耗时之和。

AT模式的适用场景与局限

AT模式适用于:同构数据库(都是MySQL或都是PostgreSQL)、单表或简单JOIN操作、对并发性能要求不是极端的场景。

不适用场景:跨异构数据库(MySQL+MongoDB)、涉及非关系型存储的操作、批量大量更新(undo_log膨胀严重)、对数据库触发器/存储过程的修改。这些场景需要使用TCC模式或Saga模式。

API接口规范方面,Seata的@GlobalTransactional只能标注在public方法上,且不支持类级别标注。如果同一个类内部方法调用(self-invocation),@GlobalTransactional不会生效——这是Spring AOP代理的限制,与@Transactional的行为一致。服务治理中需要通过Bean注入或AopContext.currentProxy()解决自调用问题。

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

(0)
小编小编
上一篇 2026年8月4日
下一篇 2026年8月4日

相关推荐

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)
小编小编
上一篇 2026年7月29日
下一篇 2026年7月29日

相关推荐