Spring Boot 3.x分布式事务实战:Seata AT模式配置与消息最终一致性方案选型

微服务拆分后的事务困境

单体应用中一个本地事务就能保证的数据一致性,在微服务架构下变成了跨库、跨服务的分布式事务问题。典型场景:订单服务创建订单后需要调用库存服务扣减库存、调用支付服务冻结资金,任何一个环节失败都需要回滚已执行的操作。Spring Boot 3.x生态下,Seata的AT模式提供了对业务代码侵入最低的方案,而基于消息队列的最终一致性方案则适合对实时性要求不高的场景。两种方案的选型和配置细节差异较大,本文逐项拆解。

Seata AT模式原理与Spring Boot 3.x集成

AT模式的核心是两阶段提交的自动化。一阶段拦截业务SQL,生成前镜像(Before Image)和后镜像(After Image)存入undo_log表,同时获取全局锁;二阶段提交则异步清理undo_log,回滚则用前镜像做补偿。业务代码只需要加一个@GlobalTransactional注解。

Seata Server部署(2.2.0版本):

# docker-compose.yml
version: '3.8'
services:
  seata-server:
    image: seataio/seata-server:2.2.0
    ports:
      - "8091:8091"
      - "7091:7091"
    environment:
      - SEATA_IP=seata-server
      - STORE_MODE=db
    volumes:
      - ./seata-config:/seata-server/resources

Spring Boot 3.x客户端配置:

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

// application.yml
seata:
  enabled: true
  application-id: order-service
  tx-service-group: my-tx-group
  service:
    vgroup-mapping:
      my-tx-group: default
  registry:
    type: nacos
    nacos:
      server-addr: nacos:8848
      namespace: seata
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: nacos:8848
      namespace: seata

每个参与分布式事务的数据库需要创建undo_log表:

-- undo_log 建表语句(MySQL)
CREATE TABLE IF NOT EXISTS undo_log (
    id            BIGINT       NOT NULL AUTO_INCREMENT,
    branch_id     BIGINT       NOT NULL,
    xid           VARCHAR(128) NOT NULL,
    context       VARCHAR(128) NOT NULL,
    rollback_info LONGBLOB     NOT NULL,
    log_status    INT          NOT NULL,
    log_created   DATETIME     NOT NULL,
    log_modified  DATETIME     NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

业务代码实战:订单-库存-支付三服务联动

订单服务中,业务入口加@GlobalTransactional注解:

@Service
@Slf4j
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private InventoryClient inventoryClient;
    @Autowired
    private PaymentClient paymentClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    @Transactional(rollbackFor = Exception.class)
    public OrderDTO createOrder(CreateOrderRequest request) {
        // 1. 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setAmount(request.getAmount());
        order.setStatus(OrderStatus.CREATED);
        orderMapper.insert(order);
        log.info("订单创建成功, orderId={}", order.getId());

        // 2. 扣减库存(远程调用)
        inventoryClient.deduct(request.getProductId(), request.getQuantity());
        log.info("库存扣减成功");

        // 3. 冻结资金(远程调用)
        paymentClient.freeze(request.getUserId(), request.getAmount());
        log.info("资金冻结成功");

        return OrderDTO.fromEntity(order);
    }
}

库存服务和支付服务的扣减/冻结方法同样需要加@Transactional注解。Seata AT模式通过DataSource代理自动拦截SQL,生成undo_log,无需业务代码手动处理回滚。当任何环节抛出异常,Seata协调器自动调用各分支的undo逻辑做补偿。

消息最终一致性方案:本地消息表+RocketMQ

AT模式虽然代码侵入低,但全局锁机制在高并发场景下会成为瓶颈。对实时性要求不严格的场景,本地消息表+消息队列的最终一致性方案更合适。核心思路:业务操作和消息记录在同一个本地事务中写入,后台定时任务扫描消息表并投递到MQ。

@Service
public class OrderServiceV2 {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OutboxMessageMapper outboxMapper;

    @Transactional(rollbackFor = Exception.class)
    public OrderDTO createOrderWithOutbox(CreateOrderRequest request) {
        // 1. 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setAmount(request.getAmount());
        order.setStatus(OrderStatus.CREATED);
        orderMapper.insert(order);

        // 2. 写入本地消息表(同一事务)
        OutboxMessage msg = new OutboxMessage();
        msg.setTopic("order-created");
        msg.setKey(String.valueOf(order.getId()));
        msg.setBody(JSON.toJSONString(new OrderEvent(order)));
        msg.setStatus(OutboxStatus.PENDING);
        msg.setRetryCount(0);
        msg.setNextRetryAt(LocalDateTime.now());
        outboxMapper.insert(msg);

        return OrderDTO.fromEntity(order);
    }
}

消息投递调度器:

@Component
@Slf4j
public class OutboxMessageScheduler {

    @Autowired
    private OutboxMessageMapper outboxMapper;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Scheduled(fixedDelay = 5000) // 每5秒扫描
    @Transactional
    public void sendPendingMessages() {
        List<OutboxMessage> messages = outboxMapper.selectPending(
            OutboxStatus.PENDING,
            LocalDateTime.now(),
            50  // 每批最多50条
        );

        for (OutboxMessage msg : messages) {
            try {
                SendResult result = rocketMQTemplate.syncSend(
                    msg.getTopic(),
                    MessageBuilder.withPayload(msg.getBody())
                        .setKeys(msg.getKey())
                        .build(),
                    3000  // 超时3秒
                );
                if (result.getSendStatus() == SendStatus.SEND_OK) {
                    msg.setStatus(OutboxStatus.SENT);
                    outboxMapper.updateById(msg);
                }
            } catch (Exception e) {
                msg.setRetryCount(msg.getRetryCount() + 1);
                if (msg.getRetryCount() >= 5) {
                    msg.setStatus(OutboxStatus.FAILED);
                    log.error("消息投递失败5次,标记为FAILED, id={}", msg.getId());
                } else {
                    // 指数退避
                    msg.setNextRetryAt(LocalDateTime.now().plusMinutes(
                        (long) Math.pow(2, msg.getRetryCount())
                    ));
                }
                outboxMapper.updateById(msg);
            }
        }
    }
}

两种方案选型对比

维度 Seata AT模式 本地消息表+MQ
一致性保证 强一致(同步回滚) 最终一致(异步补偿)
代码侵入 低(注解驱动) 中(需消息表+调度)
性能 全局锁竞争下吞吐受限 无锁,吞吐高
适用场景 资金、库存等强一致需求 通知、日志等可延迟场景
故障恢复 Seata Server自动回滚 消息重试+人工补偿

实际项目中两种方案通常并存:核心交易链路用Seata AT保证强一致,非核心的异步通知、数据同步等用消息最终一致性方案。关键是根据业务SLA明确每种场景的一致性需求,不要一刀切。

Seata生产环境踩坑记录

坑1:全局锁超时。默认全局锁超时60秒,长事务容易超时。调大lock-retry-times和lock-retry-interval:seata.client.lock.retry-times=30, seata.client.lock.retry-interval=10。

坑2:undo_log序列化兼容。字段增删改后,旧undo_log反序列化失败。上线时先禁用分布式事务,手动清理undo_log后再启用。或配置jackson序列化忽略未知属性。

坑3:Seata Server单点故障。生产环境至少部署3个Seata Server节点,使用Nacos做注册中心和配置中心,数据库存储Transaction日志。Seata Server本身无状态,任意节点宕机不影响运行中的全局事务。

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

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

相关推荐