Seata分布式事务AT模式原理剖析与微服务一致性保障实战配置

微服务架构下,原本在单机数据库中由本地事务保证的数据一致性,被拆分到多个独立服务和独立数据库中。跨服务的数据操作如果缺乏事务保障,一旦中间环节失败就会出现数据不一致。Seata作为阿里开源的分布式事务解决方案,其AT(Automatic Transaction)模式以低侵入性著称,只需在方法上添加@GlobalTransactional注解即可实现跨服务事务。本文深入AT模式原理并给出完整的Spring Cloud微服务接入实战。

Seata AT模式两阶段提交原理解析

AT模式的核心思路是”自动拦截SQL,生成回滚日志”。与XA协议的两阶段提交不同,AT模式在一阶段直接提交本地事务,释放数据库连接资源,通过undo_log表记录变更前的数据快照。二阶段如果全局事务提交,异步删除undo_log;如果回滚,根据undo_log反向补偿。

一阶段流程

1. TM(Transaction Manager)向TC(Transaction Coordinator)注册全局事务,获取XID

2. XID通过RPC调用链路传播到下游RM(Resource Manager)

3. 每个RM拦截业务SQL,解析SQL语义,查询变更前数据快照(before image)和变更后数据快照(after image)

4. 将快照存入undo_log表

5. 执行业务SQL并提交本地事务

6. 向TC注册分支事务,上报分支状态

二阶段流程

全局提交:TC通知各RM异步删除undo_log记录,因为本地事务已经提交,无需补偿。

全局回滚:TC通知各RM根据undo_log的before image反向生成补偿SQL,先校验after image与当前数据是否一致(检测脏写),一致则执行补偿,不一致说明数据已被其他事务修改,需要人工介入。

Seata Server部署与存储模式配置

Seata Server(TC)是事务协调器,独立部署。生产环境推荐使用高可用模式:

# application.yml (Seata Server)
seata:
  server:
    service:
      port: 8091
      max-commit-retry-timeout: 10000
      max-rollback-retry-timeout: 10000
    store:
      mode: db
      db:
        datasource: druid
        db-type: mysql
        url: jdbc:mysql://mysql.seata.svc:3306/seata?characterEncoding=utf8
        user: seata
        password: seata_pwd
        min-conn: 5
        max-conn: 30
        global-table: global_table
        branch-table: branch_table
        lock-table: lock_table
        distributed-lock-table: distributed_lock
    # Nacos注册中心配置
    registry:
      type: nacos
      nacos:
        application: seata-server
        server-addr: nacos.seata.svc:8848
        namespace: seata
        group: SEATA_GROUP
    # 配置中心
    config:
      type: nacos
      nacos:
        server-addr: nacos.seata.svc:8848
        namespace: seata
        group: SEATA_GROUP

存储模式选择DB而非file,保证TC宕机后事务状态不丢失。global_table存储全局事务状态,branch_table存储分支事务状态,lock_table存储全局锁。需要在数据库中执行Seata提供的建表SQL:

-- global_table
CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint DEFAULT NULL,
  `status` tinyint NOT NULL,
  `application_id` varchar(64) DEFAULT NULL,
  `transaction_service_group` varchar(64) DEFAULT NULL,
  `transaction_name` varchar(128) DEFAULT NULL,
  `timeout` int DEFAULT NULL,
  `begin_time` bigint DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_status_gmt_modified` (`status`,`gmt_modified`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 每个业务库都需要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',
  `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 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';

Spring Boot微服务接入Seata实战

以订单服务和库存服务为例,下单时需要同时扣减库存,保证两个操作的原子性。

Maven依赖:

<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>2.0.0</version>
</dependency>
<!-- 数据源代理,AT模式需要拦截SQL -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.2.20</version>
</dependency>

订单服务application.yml配置:

seata:
  enabled: true
  application-id: order-service
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: nacos.seata.svc:8848
      namespace: seata
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: nacos.seata.svc:8848
      namespace: seata
      group: SEATA_GROUP
  # AT模式数据源代理
  data-source-proxy-mode: AT
  # 关闭MyBatis的多数据源代理冲突
  use-multi-tier-data-source-proxy: false

订单服务业务代码:

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryFeignClient inventoryClient;

    // 全局事务入口
    @GlobalTransactional(timeoutMills = 60000, name = "createOrder")
    public Order createOrder(OrderDTO dto) {
        // 1. 创建订单(本地事务)
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setProductId(dto.getProductId());
        order.setQuantity(dto.getQuantity());
        order.setAmount(dto.getAmount());
        order.setStatus("CREATED");
        orderMapper.insert(order);

        // 2. 远程调用库存服务扣减库存(分支事务)
        // XID通过Feign拦截器自动传播
        Result result = inventoryClient.deduct(dto.getProductId(), dto.getQuantity());
        if (!result.isSuccess()) {
            throw new RuntimeException("库存扣减失败: " + result.getMessage());
        }

        // 3. 如果后续还有其他远程调用失败,会触发全局回滚
        // 订单插入和库存扣减都会自动回滚
        return order;
    }
}

Feign拦截器配置XID传播:

@Component
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);
            template.header("TX_XID", xid);
        }
    }
}

// Feign客户端注册拦截器
@FeignClient(name = "inventory-service", configuration = FeignConfig.class)
public interface InventoryFeignClient {
    @PostMapping("/api/inventory/deduct")
    Result deduct(@RequestParam Long productId, @RequestParam Integer quantity);
}

@Configuration
public class FeignConfig {
    @Bean
    public SeataFeignInterceptor seataFeignInterceptor() {
        return new SeataFeignInterceptor();
    }
}

AT模式全局锁与回滚机制详解

AT模式通过全局锁解决脏写问题。一阶段本地事务提交前,RM会向TC申请获取该行数据的全局锁。如果其他全局事务想要修改同一行数据,必须等待全局锁释放。

全局锁的获取在本地事务提交之前完成,流程为:

1. 拦截SQL,获取操作行的主键

2. 向TC申请这些主键的全局锁

3. 获取成功后,将before/after image写入undo_log

4. 提交本地事务

5. 向TC上报分支事务状态

如果第4步提交成功了但第5步上报失败,TC会定时重试与RM通信。如果本地事务在第4步之前失败,TC超时后回滚全局事务,无undo_log需要处理。

回滚时的脏写检测:RM执行补偿前,先用after image与当前数据库中的数据做比对。如果一致,说明中间没有其他事务修改过该数据,可以安全回滚。如果不一致,说明存在脏写,Seata会记录日志并停止自动回滚,转人工处理。

分布式事务性能调优与避坑指南

全局事务超时设置:默认60000ms(60秒)。超时后TC自动发起回滚。如果业务链路较长或包含慢调用,需要适当增大超时时间。但超时过长会导致锁长时间占用,影响并发性能。

// 控制超时的粒度
@GlobalTransactional(timeoutMills = 120000, name = "createOrder")
// rollbackFor属性指定哪些异常触发回滚
@GlobalTransactional(timeoutMills = 60000, rollbackFor = Exception.class, name = "payment")

避免大事务:不要在全局事务中执行耗时操作(如文件上传、外部HTTP调用、批量数据处理)。长事务持锁时间长,极易导致锁等待和超时回滚。将非核心操作移到事务外,通过补偿机制处理。

读写隔离:AT模式默认读未提交(Read Uncommitted)。如果读操作也在全局事务中且需要读到已提交数据,使用@GlobalLock + SELECT FOR UPDATE:

@GlobalTransactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // 使用@GlobalLock注解获取全局锁,保证读到最新已提交数据
    Account from = accountMapper.selectForUpdate(fromId);  // SELECT ... FOR UPDATE
    Account to = accountMapper.selectForUpdate(toId);

    if (from.getBalance().compareTo(amount) < 0) {
        throw new RuntimeException("余额不足");
    }

    accountMapper.deduct(fromId, amount);
    accountMapper.add(toId, amount);
}

undo_log清理:全局事务提交后,undo_log由Seata异步清理。如果高频事务,undo_log表会快速增长。Seata提供定时清理任务,也可以手动清理已完成的记录:

-- 清理已完成且超过保留期的undo_log
DELETE FROM undo_log
WHERE log_created < DATE_SUB(NOW(), INTERVAL 7 DAY)
AND branch_id IN (
  SELECT branch_id FROM seata.branch_table
  WHERE status != 0  -- 非进行中的分支
);

Seata AT模式适合强一致性要求较高且可以容忍短时间锁等待的业务场景,如订单创建与库存扣减、转账与账户更新。对于最终一致性可接受的场景(如消息通知、积分发放),更适合使用本地消息表或Seata的TCC/SAGA模式。技术选型时需要根据业务特点权衡,不要所有接口都套用分布式事务。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/seata-fen-bu-shi-shi-wu-at-mo-shi-yuan-li-pou-xi-yu-wei-fu/

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

相关推荐