Spring Boot分布式事务一致性方案选型:2PC、TCC、Saga与消息表对比

分布式事务的典型场景与挑战

微服务架构下,一个业务操作往往跨越多个服务,每个服务维护自己的数据库实例。例如电商下单流程涉及:订单服务创建订单、库存服务扣减库存、账户服务扣减余额。三步操作要么全部成功,要么全部回滚。但各服务数据库独立,无法使用本地事务保证一致性——这就是分布式事务问题。

常见的分布式事务方案有四种:2PC(两阶段提交)、TCC(Try-Confirm-Cancel)、Saga(长事务编排)、本地消息表+最终一致性。没有银弹,方案选择取决于业务对一致性的容忍度和系统复杂度承受能力。

2PC两阶段提交与XA事务

2PC是最严格的分布式事务方案,通过协调者(Coordinator)统一控制所有参与者的提交或回滚。第一阶段(Prepare)各参与者执行事务但不提交,将结果告知协调者;第二阶段(Commit/Abort)协调者根据第一阶段结果决定全局提交或回滚。XA协议是2PC在数据库层面的标准实现,MySQL、PostgreSQL均支持。

# Spring Boot + Atomikos XA事务配置
# pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

# application.yml
spring:
  jta:
    atomikos:
      datasource:
        order-db:
          unique-resource-name: orderDataSource
          xa-properties:
            url: jdbc:mysql://order-db:3306/order
            user: root
            password: xxx
          pool-size: 10
        account-db:
          unique-resource-name: accountDataSource
          xa-properties:
            url: jdbc:mysql://account-db:3306/account
            user: root
            password: xxx
          pool-size: 10

# OrderService.java
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private AccountClient accountClient;
    @Autowired
    private InventoryClient inventoryClient;

    @Transactional  // JTA全局事务
    public void createOrder(Order order) {
        orderMapper.insert(order);
        accountClient.deduct(order.getUserId(), order.getAmount());
        inventoryClient.deduct(order.getProductId(), order.getQuantity());
    }
}

2PC的缺点在于同步阻塞——Prepare阶段所有参与者持有锁不释放,直到第二阶段完成。如果协调者宕机,参与者会长时间阻塞等待,导致资源锁定。因此2PC只适合事务参与者少、执行时间短的场景,不推荐用于跨多个服务的长流程。

TCC模式与Seata AT实现

TCC将业务拆为三个阶段:Try(预留资源)、Confirm(确认提交)、Cancel(取消预留)。与2PC不同,TCC的Try阶段不持有数据库锁,而是通过业务逻辑预留资源——比如扣减库存时先冻结而非真正扣减。这降低了对数据库的锁定时间,但要求每个业务接口都实现三套方法,开发成本高。

Seata的AT模式在TCC基础上做了自动化——框架自动生成回滚日志(undo_log),业务代码只需要写正常分支逻辑。一阶段执行业务SQL并记录前镜像(before image),二阶段根据全局事务结果自动提交或用前镜像做反向补偿。

// Seata AT模式配置
// 1. 引入依赖
// implementation 'io.seata:seata-spring-boot-starter:2.0.0'

// 2. 在业务入口标注全局事务
@GlobalTransactional(timeoutMills = 60000, name = "create-order")
public void createOrder(OrderDTO dto) {
    // 以下每个远程调用自动纳入Seata全局事务
    orderService.create(dto);           // 一阶段:插入订单记录+记录undo_log
    inventoryService.deduct(dto.getProductId(), dto.getQty());  // 冻结库存
    accountService.deduct(dto.getUserId(), dto.getAmount());   // 冻结余额
    // 全部成功→自动提交;任一失败→Seata根据undo_log自动补偿回滚
}

// 3. 每个微服务数据库建undo_log表
// CREATE TABLE undo_log (
//   id BIGINT PRIMARY KEY AUTO_INCREMENT,
//   branch_id BIGINT NOT NULL,
//   xid VARCHAR(100) 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,
//   UNIQUE KEY ux_undo_log (xid, branch_id)
// );

Saga模式与长事务编排

Saga将长事务拆为一串本地事务,每个本地事务提交后触发下一个步骤。如果某步骤失败,反向执行已完成步骤的补偿操作。相比TCC,Saga没有全局锁,每个步骤完成后立即释放资源,吞吐更高但一致性更弱——中间状态可被外部观察到。

Saga有两种编排方式:集中式(Orchestration)由一个协调器控制流程,各参与者只实现正向和补偿操作;事件驱动(Choreography)通过消息队列串联,每个步骤完成后发布事件触发下一环节。集中式逻辑清晰易调试,适合流程固定且步骤不超过10个的场景。

// Seata Saga状态机定义(JSON DSL)
// resources/statelang/create_order.json
{
  "Name": "createOrder",
  "Comment": "创建订单Saga流程",
  "StartState": "CreateOrder",
  "States": {
    "CreateOrder": {
      "Type": "ServiceTask",
      "ServiceName": "orderService",
      "ServiceMethod": "create",
      "CompensateState": "CancelOrder",
      "Next": "DeductInventory",
      "Input": ["$.orderDTO"],
      "Output": { "orderId": "$.orderId" }
    },
    "DeductInventory": {
      "Type": "ServiceTask",
      "ServiceName": "inventoryService",
      "ServiceMethod": "deduct",
      "CompensateState": "CompensateInventory",
      "Next": "DeductAccount",
      "Input": ["$.orderDTO.productId", "$.orderDTO.qty"]
    },
    "DeductAccount": {
      "Type": "ServiceTask",
      "ServiceName": "accountService",
      "ServiceMethod": "deduct",
      "CompensateState": "CompensateAccount",
      "Input": ["$.orderDTO.userId", "$.orderDTO.amount"],
      "IsEnd": true
    },
    "CancelOrder": {
      "Type": "ServiceTask",
      "ServiceName": "orderService",
      "ServiceMethod": "cancel",
      "Input": ["$.orderId"],
      "IsEnd": true
    },
    "CompensateInventory": {
      "Type": "ServiceTask",
      "ServiceName": "inventoryService",
      "ServiceMethod": "compensate"
    },
    "CompensateAccount": {
      "Type": "ServiceTask",
      "ServiceName": "accountService",
      "ServiceMethod": "compensate"
    }
  }
}

本地消息表实现最终一致性

本地消息表是最轻量的分布式事务方案——不需要引入任何事务框架,利用本地事务+消息重试保证最终一致性。核心思路:业务操作和消息记录在同一个本地事务中写入,后台定时任务扫描消息表,将未发送的消息投递到MQ,消费者处理后更新消息状态。

// 本地消息表方案实现
// 1. 消息表结构
// CREATE TABLE outbox_message (
//   id BIGINT PRIMARY KEY AUTO_INCREMENT,
//   biz_type VARCHAR(64) NOT NULL,
//   biz_id VARCHAR(128) NOT NULL,
//   content JSON NOT NULL,
//   status TINYINT DEFAULT 0,   -- 0待发送 1已发送 2失败
//   retry_count INT DEFAULT 0,
//   max_retry INT DEFAULT 5,
//   created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
//   INDEX idx_status_created (status, created_at)
// );

@Service
@Slf4j
public class OrderService {

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

    @Transactional
    public void createOrder(Order order) {
        // 业务操作
        orderMapper.insert(order);

        // 同一事务写入消息表
        OutboxMessage msg = new OutboxMessage();
        msg.setBizType("ORDER_CREATED");
        msg.setBizId(order.getId().toString());
        msg.setContent(JSON.toJSONString(order));
        msg.setStatus(0);
        outboxMapper.insert(msg);
        // 本地事务提交——业务和消息要么同时成功,要么同时回滚
    }
}

// 消息投递定时任务
@Component
@Slf4j
public class OutboxMessageSender {

    @Autowired
    private OutboxMessageMapper outboxMapper;
    @Autowired
    private RocketMQTemplate mqTemplate;

    @Scheduled(fixedDelay = 1000)  // 每秒扫描一次
    public void sendPendingMessages() {
        List<OutboxMessage> messages = outboxMapper.selectPending(100);
        for (OutboxMessage msg : messages) {
            try {
                mqTemplate.convertAndSend(msg.getBizType(), msg.getContent());
                outboxMapper.updateStatus(msg.getId(), 1);  // 标记已发送
            } catch (Exception e) {
                outboxMapper.incrementRetry(msg.getId());
                if (msg.getRetryCount() + 1 >= msg.getMaxRetry()) {
                    outboxMapper.updateStatus(msg.getId(), 2);  // 标记失败
                    log.error("Message {} exceeded max retry", msg.getId());
                }
            }
        }
    }
}

方案选型决策框架

四种方案的选择不是互斥的,同一系统中不同业务流程可以用不同方案。决策依据三个维度:一致性要求(强一致vs最终一致)、业务流程长度(3步以内vs5步以上)、基础设施复杂度容忍度。

2PC/XA:适合2-3个数据库之间的短事务,一致性要求极强,但吞吐低、阻塞风险高。TCC/Seata AT:适合3-5个服务的中等长度流程,需要Seata基础设施但开发侵入性适中。Saga:适合5步以上的长流程(如订单全流程包含支付、物流、退款),容忍短暂不一致。本地消息表:适合对端是异步消费者的场景(如下单后触发积分发放、短信通知),实现最简单但只能保证最终一致性。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-fen-bu-shi-shi-wu-yi-zhi-xing-fang-an-xuan-xing-2/

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

相关推荐