Spring Boot微服务分布式事务Seata实战:AT模式与TCC混合方案

订单-库存-支付三服务分布式事务的Seata AT模式实战,包括全局锁机制、TCC自定义补偿、Seata Server部署与监控告警体系搭建。

微服务分布式事务为什么难做

单体应用中,事务由数据库的ACID机制保证,一个@Transactional注解就能解决数据一致性问题。拆分成微服务后,一个业务操作可能横跨订单、库存、支付三个服务,每个服务有自己的数据库实例。跨库事务不能依赖数据库本地事务机制,必须引入分布式事务框架来协调多个参与者的提交和回滚。

分布式事务的核心难题是部分失败:订单服务成功了,库存服务回滚了,支付服务超时了——系统处于不一致的中间状态,必须有机制能够自动恢复到一致状态。Seata是阿里开源的分布式事务框架,提供AT、TCC、SAGA、XA四种模式,其中AT模式对业务代码侵入最小,适合大多数Spring Boot微服务场景。

Seata AT模式工作原理

AT模式的全称是Automatic Transaction,核心思路是两阶段提交的自动版:

一阶段:拦截业务SQL,在执行前保存修改前的数据快照(before-image),执行后保存修改后的数据快照(after-image),生成行锁。快照和行锁保存在每个服务数据库的undo_log表中。

二阶段提交:删除undo_log和行锁。

二阶段回滚:读取undo_log中的before-image,反向补偿SQL将数据恢复到修改前的状态。

整个过程中,业务代码只需要添加一个@GlobalTransactional注解,不需要手动编写补偿逻辑。Seata通过数据源代理自动拦截SQL、记录快照、管理行锁。

Seata Server部署与配置

Seata Server(TC——Transaction Coordinator)是事务协调器,独立部署,负责管理全局事务的提交和回滚决策。

# docker-compose部署Seata Server
version: '3.8'
services:
  seata-server:
    image: seataio/seata-server:2.2.0
    container_name: seata-server
    ports:
      - "8091:8091"
      - "7091:7091"
    environment:
      - SEATA_IP=seata-server
      - STORE_MODE=db
    volumes:
      - ./seata-config:/seata-server/resources
    depends_on:
      - mysql
    networks:
      - microservice-net

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: seata
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
    networks:
      - microservice-net

networks:
  microservice-net:
    driver: bridge
# seata-config/application.yml——Seata Server配置
server:
  port: 8091

seata:
  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://mysql:3306/seata?rewriteBatchedStatements=true
      username: root
      password: root123
      min-conn: 5
      max-conn: 30
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock

  registry:
    type: nacos
    nacos:
      server-addr: nacos:8848
      namespace: seata
      group: SEATA_GROUP
      application: seata-server

  config:
    type: nacos
    nacos:
      server-addr: nacos:8848
      namespace: seata
      group: SEATA_GROUP

Spring Boot微服务集成Seata

每个参与分布式事务的微服务需要完成以下配置:引入Seata客户端依赖、配置数据源代理、创建undo_log表、注册到Seata Server。

<!-- pom.xml依赖 -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  <version>2023.0.1.2</version>
</dependency>
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</version>
  <version>2.2.0</version>
</dependency>
# application.yml——Seata客户端配置
seata:
  enabled: true
  application-id: order-service
  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: seata
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: seata
      group: SEATA_GROUP
-- undo_log表DDL——每个业务数据库都需要创建
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(11)      NOT NULL COMMENT '0:normal, 1:defensed',
  `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';

订单-库存-支付三服务分布式事务实战

以电商下单场景为例,用户点击”确认支付”后,需要同时完成三个操作:创建订单、扣减库存、发起支付。任何一步失败都需要全部回滚。

// 订单服务——全局事务发起方
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private InventoryClient inventoryClient;

    @Autowired
    private PaymentClient paymentClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public Order createOrder(OrderDTO orderDTO) {
        // 1. 创建订单(本地事务)
        Order order = new Order();
        order.setUserId(orderDTO.getUserId());
        order.setProductId(orderDTO.getProductId());
        order.setQuantity(orderDTO.getQuantity());
        order.setAmount(orderDTO.getAmount());
        order.setStatus(OrderStatus.CREATED);
        orderMapper.insert(order);

        // 2. 扣减库存(远程调用,Seata自动纳入全局事务)
        inventoryClient.deduct(
            order.getProductId(),
            order.getQuantity()
        );

        // 3. 发起支付(远程调用)
        paymentClient.pay(
            order.getId(),
            order.getAmount()
        );

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

        return order;
    }
}
// 库存服务——事务分支
@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 == null) {
            throw new BusinessException("Product not found");
        }
        if (inventory.getStock() < quantity) {
            throw new BusinessException("Insufficient stock");
        }
        // Seata AT模式会自动记录before/after image
        inventory.setStock(inventory.getStock() - quantity);
        inventoryMapper.updateById(inventory);
    }
}
// 支付服务——事务分支
@Service
public class PaymentService {

    @Autowired
    private PaymentMapper paymentMapper;

    @Autowired
    private AccountClient accountClient;

    @Transactional(rollbackFor = Exception.class)
    public void pay(Long orderId, BigDecimal amount) {
        // 创建支付记录
        Payment payment = new Payment();
        payment.setOrderId(orderId);
        payment.setAmount(amount);
        payment.setStatus(PaymentStatus.PROCESSING);
        paymentMapper.insert(payment);

        // 调用账户扣款
        accountClient.deductAmount(amount);

        payment.setStatus(PaymentStatus.SUCCESS);
        paymentMapper.updateById(payment);
    }
}

全局锁与写隔离

AT模式下,Seata通过全局锁机制避免脏写问题。当全局事务T1正在修改某行数据时,全局锁会阻止其他全局事务T2同时修改同一行。但本地事务不受全局锁约束——如果一个非Seata管理的本地事务同时修改该行,就会出现脏写。

解决方案是在代理数据源层面拦截写操作,或者在数据库层面添加for update行锁。Seata 2.x已经支持通过配置强制全局锁检查:

# 强制全局锁检查配置
seata:
  client:
    rm:
      lock-select-for-update: true
      report-retry-count: 5
      report-success-enable: true
      async-commit-buffer-limit: 10000

Seata TCC模式处理复杂业务补偿

AT模式适合简单的CRUD场景,但如果业务操作不是简单的SQL增删改(如调用第三方支付、发送消息等),AT模式无法自动生成补偿SQL,需要使用TCC模式手动定义Try/Confirm/Cancel三个阶段的逻辑。

// TCC模式——自定义三阶段逻辑
@LocalTCC
public interface PaymentTccService {

    @TwoPhaseBusinessAction(
        name = "paymentTcc",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryPay(
        @BusinessActionContextParameter(paramName = "orderId") Long orderId,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount
    );

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

@Service
public class PaymentTccServiceImpl implements PaymentTccService {

    @Autowired
    private PaymentMapper paymentMapper;
    @Autowired
    private AccountClient accountClient;

    @Override
    public boolean tryPay(Long orderId, BigDecimal amount) {
        // Try阶段:冻结资金,创建待确认支付记录
        Payment payment = new Payment();
        payment.setOrderId(orderId);
        payment.setAmount(amount);
        payment.setStatus(PaymentStatus.FROZEN);  // 冻结状态
        paymentMapper.insert(payment);

        // 冻结账户余额
        accountClient.freeze(amount);
        return true;
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        // Confirm阶段:扣减冻结资金,支付成功
        Long orderId = context.getActionContext("orderId", Long.class);
        Payment payment = paymentMapper.selectByOrderId(orderId);
        payment.setStatus(PaymentStatus.SUCCESS);
        paymentMapper.updateById(payment);

        // 扣减冻结资金
        accountClient.deductFrozen(payment.getAmount());
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        // Cancel阶段:解冻资金,支付取消
        Long orderId = context.getActionContext("orderId", Long.class);
        Payment payment = paymentMapper.selectByOrderId(orderId);
        if (payment != null) {
            payment.setStatus(PaymentStatus.CANCELLED);
            paymentMapper.updateById(payment);

            // 解冻账户余额
            accountClient.unfreeze(payment.getAmount());
        }
        return true;
    }
}

分布式事务监控与问题诊断

Seata事务在生产环境中最常见的问题是全局事务超时和回滚失败。需要建立监控和告警体系:

# Prometheus监控Seata指标
# 全局事务成功率
sum(rate(seata_transaction_total{status="committed"}[5m]))
/
sum(rate(seata_transaction_total[5m]))

# 全局事务平均耗时
histogram_quantile(0.95,
  sum(rate(seata_transaction_duration_seconds_bucket[5m])) by (le)
)

# 回滚失败告警
sum(increase(seata_transaction_total{status="rollback_failed"}[5m])) > 0
常见问题 原因 排查方向
全局事务超时 下游服务响应慢 检查分支事务执行日志,调整超时时间
undo_log残留 回滚时网络中断 手动清理过期undo_log,检查TC日志
全局锁等待超时 长事务占用锁 缩短事务范围,添加锁重试配置
回滚补偿失败 数据已被其他事务修改 检查脏写场景,启用lock-select-for-update

Seata AT模式通过自动化SQL拦截和快照回滚,把分布式事务的复杂度从业务代码中剥离出来。但自动化不代表零运维——全局锁冲突、回滚补偿失败、undo_log膨胀这些问题需要配套的监控告警和应急处理流程。TCC模式虽然侵入性更强,但在涉及外部系统交互和复杂业务补偿的场景下仍然是必要的。AT和TCC可以在同一个系统中混合使用,根据每个事务分支的特征选择最合适的模式。

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

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

相关推荐