springboot 业务系统和流程系统两个微服务之间的事务如何保障

在 Spring Boot 微服务架构中,业务系统和流程系统之间的事务保障核心是解决分布式事务问题(跨服务数据一致性)。由于微服务独立部署、数据隔离,传统单机事务(ACID)无法直接生效,需采用分布式事务方案。以下是结合实际场景的落地方案,从简单到复杂、从弱一致性到强一致性逐步说明:

一、核心问题与设计原则

1. 核心矛盾

  • 跨服务调用:业务系统(如订单服务)调用流程系统(如审批流程服务),两个服务操作各自数据库,无法用单一事务控制。
  • 网络不可靠:服务调用可能超时、宕机,导致一方提交、一方未提交的 “数据不一致”。

2. 设计原则

  • 优先选最终一致性:大多数业务场景(如订单创建 + 流程启动)无需实时强一致,最终一致即可降低系统复杂度。
  • 避免长事务:微服务事务应 “短平快”,减少锁竞争和超时风险。
  • 失败可补偿:设计 “回滚 / 补偿机制”,确保异常时数据能恢复或修正。

二、落地方案(按优先级排序)

方案 1:本地消息表 + 定时任务(最常用,最终一致性)

原理

基于 “可靠消息投递” 思想:业务系统执行本地事务时,同时写入 “消息表”(与业务表同库,利用本地事务保证原子性),再通过定时任务将消息投递到流程系统,流程系统执行完成后回调确认,未确认的消息重试。

流程拆解(以 “订单创建后启动审批流程” 为例)

步骤业务系统(订单服务)流程系统(审批服务)
1 开启本地事务:
 
- 插入订单表(订单状态:待审批)
 
- 插入消息表(消息状态:待发送,内容:订单 ID、流程类型)
-
2 提交本地事务(订单和消息表原子提交,要么都成功) -
3 定时任务(如 Quartz、Spring Scheduler)扫描 “待发送” 消息,调用流程系统接口 接收消息,开启本地事务:
 
- 插入流程实例(关联订单 ID)
 
- 更新流程状态:已启动
4 流程系统执行成功后,调用业务系统 “消息确认接口” 提交本地事务
5 业务系统更新消息表状态为 “已确认” -
6 失败重试:定时任务对 “待发送” 且超时的消息重试(设置重试次数 / 间隔,避免死循环) 幂等处理:接收重复消息时,通过订单 ID 判断流程是否已创建,避免重复执行

关键实现要点

  • 消息表设计:id(主键)、business_id(业务 ID,如订单 ID)、message_type(消息类型)、status(待发送 / 发送中 / 已确认 / 失败)、content(消息内容 JSON)、retry_count(重试次数)、create_timeupdate_time
  • 幂等性:流程系统必须通过business_id做幂等校验(如查询订单对应的流程是否已存在),避免重复创建流程。
  • 重试策略: exponential backoff(指数退避),如 10s、30s、1min、5min,重试 3-5 次后标记为 “失败”,人工介入。

优点

  • 实现简单,无中间件依赖,稳定性高(依赖数据库本地事务)。
  • 适合中小规模系统、非核心链路(如通知、日志同步)。

缺点

  • 侵入业务代码(需手动操作消息表)。
  • 一致性延迟:依赖定时任务间隔,存在短暂数据不一致。

方案 2:RocketMQ/RabbitMQ 事务消息(最终一致性,无侵入)

原理

利用中间件的 “事务消息” 特性,替代方案 1 的 “本地消息表”,将消息投递与本地事务绑定,无需手动维护消息表,降低代码侵入性。

核心流程(以 RocketMQ 为例)

  1. 业务系统发送 “半事务消息”:向 RocketMQ 发送一条 “未确认” 的消息(半消息),RocketMQ 收到后存储,但不投递到消费者(流程系统)。
  2. 业务系统执行本地事务:插入订单表(待审批),执行成功 / 失败。
  3. 业务系统提交 “消息确认”:
    • 本地事务成功:向 RocketMQ 发送 “确认投递” 指令,RocketMQ 将消息标记为可消费,流程系统接收。
    • 本地事务失败:向 RocketMQ 发送 “取消投递” 指令,RocketMQ 删除半消息。
  4. RocketMQ 回查机制:若业务系统未返回确认(如宕机),RocketMQ 会定时回查业务系统的事务状态,根据结果决定是否投递。
  5. 流程系统消费消息:接收消息后,创建流程实例,执行本地事务,完成后返回消费成功(RocketMQ 删除消息);失败则重试(需幂等处理)。

关键实现要点

  • 依赖中间件支持:RocketMQ 原生支持事务消息,RabbitMQ 需通过 “死信队列 + 确认机制” 模拟。
  • 事务回查接口:业务系统需提供回查接口(如/transaction/check?businessId=xxx),RocketMQ 通过该接口确认事务状态。
  • 幂等性:流程系统同样需通过businessId校验,避免重复消费。

优点

  • 无代码侵入(无需手动维护消息表),开发效率高。
  • 一致性比方案 1 更及时(无需定时任务扫描,回查机制保障)。

缺点

  • 依赖中间件(需部署 RocketMQ),增加系统复杂度。
  • 适合核心链路(如订单创建、支付回调)。

方案 3:Saga 模式(长事务 / 复杂流程,最终一致性)

适用场景

当业务流程包含多个跨服务步骤(如 “订单创建→流程启动→库存扣减→支付确认”),且每个步骤都有 “正向操作” 和 “反向补偿操作” 时,适合用 Saga 模式。

原理

将分布式事务拆分为多个 “本地事务”(每个服务的操作),每个本地事务对应一个 “补偿事务”(回滚操作),通过事件驱动或编排模式执行,若某一步失败,触发前面所有步骤的补偿操作,恢复数据一致性。

两种实现方式

(1)编排式 Saga(推荐,适合流程固定场景)
由一个 “Saga 协调器”(如 Spring Cloud Stream、Seata Saga)统一调度所有服务的步骤,流程清晰,易于维护。
流程示例(订单 + 流程 + 库存):
  1. 协调器调用业务系统:执行 “创建订单”(本地事务成功)。
  2. 协调器调用流程系统:执行 “启动流程”(本地事务成功)。
  3. 协调器调用库存系统:执行 “扣减库存”(本地事务成功)。
  4. 若步骤 3 失败(如库存不足),协调器触发补偿:
    • 调用流程系统:“撤销流程”(补偿事务)。
    • 调用业务系统:“取消订单”(补偿事务)。
(2) choreography 式 Saga(无协调器,适合灵活场景)
每个服务完成本地事务后,发送事件通知下一个服务执行,失败时发送补偿事件,由其他服务监听并执行补偿。
流程示例:
  1. 业务系统创建订单→发送OrderCreatedEvent
  2. 流程系统监听OrderCreatedEvent→启动流程→发送ProcessStartedEvent
  3. 库存系统监听ProcessStartedEvent→扣减库存→发送StockDeductedEvent
  4. 若库存扣减失败→发送StockDeductFailedEvent
  5. 流程系统监听StockDeductFailedEvent→撤销流程→发送ProcessCancelledEvent
  6. 业务系统监听ProcessCancelledEvent→取消订单。

关键实现要点

  • 补偿事务设计:每个正向操作必须有对应的反向操作(如 “启动流程”→“撤销流程”,“创建订单”→“取消订单”),补偿操作需保证幂等性。
  • 状态追踪:需记录 Saga 执行状态(如 “订单创建成功→流程启动中→库存扣减失败”),便于问题排查和手动干预。
  • 技术选型:
    • 编排式:Seata Saga(轻量级,支持 XML / 注解配置)、Camunda(流程引擎,适合复杂流程)。
    • choreography 式:Spring Cloud Stream(事件驱动)、Kafka(消息队列)。

优点

  • 支持长事务、复杂跨服务流程。
  • 失败后通过补偿机制恢复一致性,不阻塞业务。

缺点

  • 设计复杂(需维护正向 / 反向操作)。
  • 仅支持最终一致性,不适合强一致场景。

方案 4:2PC/3PC(强一致性,不推荐微服务)

原理

  • 2PC(两阶段提交):分为 “准备阶段” 和 “提交阶段”,由事务协调器统一控制所有服务:
    1. 准备阶段:协调器向业务系统、流程系统发送 “准备指令”,两服务执行本地事务(不提交),返回 “就绪” 或 “失败”。
    2. 提交阶段:若所有服务就绪,协调器发送 “提交指令”;若任一服务失败,发送 “回滚指令”。
  • 3PC(三阶段提交):在 2PC 基础上增加 “预提交阶段”,降低阻塞风险,但复杂度更高。

为什么不推荐微服务?

  • 阻塞问题:准备阶段所有服务持有数据库锁,若某服务宕机,其他服务会一直阻塞,导致系统可用性极低。
  • 一致性风险:协调器宕机后,可能出现部分服务提交、部分回滚的情况。
  • 微服务理念冲突:微服务强调 “去中心化”,2PC/3PC 需要强中心化协调,违背微服务设计原则。

适用场景

仅适合数据库层面紧密耦合的场景(如单体拆分为微服务过渡期),纯微服务架构中几乎不用。

三、技术选型建议

方案一致性复杂度适用场景技术栈
本地消息表 + 定时任务 最终一致(延迟低) 中小系统、非核心链路 Spring Boot + 数据库 + Quartz
RocketMQ 事务消息 最终一致(延迟极低) 核心链路、无代码侵入需求 Spring Boot + RocketMQ
Saga 模式 最终一致(支持复杂流程) 长事务、多步骤跨服务流程 Seata Saga / Camunda + Spring Cloud Stream
2PC/3PC 强一致 中高 数据库紧耦合场景(不推荐) Spring Cloud Alibaba Seata(支持 2PC)

四、关键保障措施(无论选哪种方案)

1. 幂等性设计(必须实现)

  • 所有跨服务接口必须支持幂等,避免重复执行导致数据错误。
  • 实现方式:通过businessId(如订单 ID)作为唯一标识,查询操作是否已执行(如流程系统查询 “订单 ID=123 的流程是否存在”)。

2. 超时处理

  • 服务调用设置超时时间(如 Feign 调用设置ribbon.ReadTimeout=3000ms),避免无限阻塞。
  • 超时后默认视为失败,触发重试或补偿机制。

3. 日志与监控

  • 记录分布式事务关键日志(如消息发送 / 接收、事务状态、补偿执行结果),便于问题排查。
  • 监控事务成功率、重试次数、失败率(如 Prometheus + Grafana),异常时告警(如钉钉、邮件)。

4. 手动干预机制

  • 对于重试多次仍失败的事务(如流程启动失败),需提供手动处理入口(如后台管理系统的 “重试流程”“取消订单” 按钮),避免数据长期不一致。

五、代码示例(方案 2:RocketMQ 事务消息)

1. 依赖引入(Spring Boot + RocketMQ)

xml
 
 
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.3</version>
</dependency>
 

2. 业务系统配置(生产者)

yaml
 
 
rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: order-transaction-group # 事务生产者组
 

3. 业务系统事务消息发送

java
 
运行
 
 
 
 
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class OrderService {

    @Resource
    private OrderMapper orderMapper; // 订单DAO
    @Resource
    private RocketMQTemplate rocketMQTemplate;

    // 发送事务消息+执行本地事务
    public void createOrderAndStartProcess(OrderDTO orderDTO) {
        // 1. 发送半事务消息(topic: process_topic,tag: start)
        String businessId = orderDTO.getOrderId(); // 业务唯一ID
        String messageContent = JSON.toJSONString(orderDTO); // 消息内容

        rocketMQTemplate.sendMessageInTransaction(
            "process_topic:start", // topic+tag
            MessageBuilder.withPayload(messageContent)
                .setHeader(RocketMQHeaders.TRANSACTION_ID, businessId)
                .build(),
            orderDTO // 传递给本地事务的参数
        );
    }

    // 2. 本地事务执行逻辑(由RocketMQ回调)
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        OrderDTO orderDTO = (OrderDTO) arg;
        try {
            // 执行本地事务:插入订单表
            Order order = new Order();
            order.setOrderId(orderDTO.getOrderId());
            order.setStatus("PENDING_APPROVAL"); // 待审批
            orderMapper.insert(order);
            return RocketMQLocalTransactionState.COMMIT; // 本地事务成功,确认投递
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK; // 本地事务失败,取消投递
        }
    }

    // 3. 事务回查逻辑(RocketMQ回查时调用)
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String businessId = msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID).toString();
        // 回查订单状态:是否已创建
        Order order = orderMapper.selectById(businessId);
        if (order != null) {
            return RocketMQLocalTransactionState.COMMIT; // 订单已创建,确认投递
        } else {
            return RocketMQLocalTransactionState.ROLLBACK; // 订单未创建,取消投递
        }
    }
}
 

4. 流程系统消费消息(消费者)

java
 
运行
 
 
 
 
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
@RocketMQMessageListener(
    topic = "process_topic",
    selectorExpression = "start", // 只消费tag=start的消息
    consumerGroup = "process-consumer-group"
)
public class ProcessConsumer implements RocketMQListener<String> {

    @Resource
    private ProcessMapper processMapper; // 流程DAO

    @Override
    @Transactional
    public void onMessage(String message) {
        OrderDTO orderDTO = JSON.parseObject(message, OrderDTO.class);
        String businessId = orderDTO.getOrderId();

        // 幂等校验:是否已创建流程
        ProcessInstance process = processMapper.selectByBusinessId(businessId);
        if (process != null) {
            return; // 已创建,直接返回
        }

        // 执行本地事务:创建流程实例
        ProcessInstance newProcess = new ProcessInstance();
        newProcess.setBusinessId(businessId);
        newProcess.setProcessType("ORDER_APPROVAL");
        newProcess.setStatus("RUNNING"); // 流程运行中
        processMapper.insert(newProcess);
    }
}
 

总结

  1. 大多数场景优先选择RocketMQ 事务消息(无侵入、一致性及时)或本地消息表(简单稳定),满足最终一致性需求。
  2. 复杂跨服务流程(多步骤、需补偿)选择Saga 模式,通过正向 / 反向操作保障一致性。
  3. 强一致性需求极少(微服务架构不推荐),若必须,可考虑 Seata 的 2PC 模式,但需接受可用性降低的代价。
  4. 无论哪种方案,幂等性、超时处理、日志监控是必做的基础保障,否则会出现数据不一致或排查困难的问题。
posted @ 2025-11-21 11:09  hanease  阅读(34)  评论(0)    收藏  举报