深入理解微服务分布式事务:Saga 模式详解与实战
深入理解微服务分布式事务:Saga 模式详解与实战
在微服务架构中,系统被拆分成多个独立的服务,这种拆分带来了灵活性,但也打破了单体应用中数据库 ACID 事务的边界。传统的两阶段提交(2PC/XA)因性能差和锁竞争问题,已不再适用于高并发的微服务场景。
Saga 模式作为一种最终一致性的解决方案,成为了处理长流程分布式事务的首选策略。
什么是 Saga 模式?
Saga 模式将一个长事务拆分成一系列本地事务(Local Transaction)。每个本地事务更新单个服务的数据,并发布消息或事件来触发下一个本地事务。如果某个步骤失败,Saga 会执行一系列补偿事务(Compensating Transaction),以此回滚之前的操作,确保系统最终处于一致状态。
Saga 并不保证隔离性(Isolation),用户可能会在事务中间状态看到未完成的数据(例如:订单已创建但未扣减库存),这符合 BASE 理论(基本可用、软状态、最终一致性)。
Saga 的两种核心实现模式
Saga 主要有两种协调方式:协同式(Choreography) 和 编排式(Orchestration)。
1. 协同式(Choreography)—— 基于事件
没有中央协调者,每个参与的服务在完成本地事务后发布领域事件,其他服务订阅该事件并作出反应。
- 优点:简单,服务间解耦,适合参与者较少的简单业务。
- 缺点:业务流程分散在各个服务中,难以全局追踪,容易产生循环依赖。
2. 编排式(Orchestration)—— 基于命令
引入一个Saga 协调器(Orchestrator)(通常由发起事务的服务承担)。协调器负责告诉每个参与者该做什么(发送命令),并根据参与者的反馈(成功或失败)决定下一步是继续还是回滚。
- 优点:流程逻辑集中,易于维护和监控,避免循环依赖。
- 缺点:协调器可能成为单点瓶颈(需高可用设计)。
实战:基于 Spring Cloud Stream 的协同式 Saga(函数式编程版)
注:以下代码采用 Spring Cloud Stream 目前推荐的函数式编程风格(Functional Style)
场景定义
- 订单服务:创建订单(步骤 A)
- 库存服务:扣减库存(步骤 B)
- 支付服务:扣减余额(步骤 C)
- 物流服务:创建运单(步骤 D,假设此处失败)
核心实现逻辑
1. 消息通道配置 (application.yml)
使用函数式绑定名称:
spring:
cloud:
function:
definition: checkInventory;handleInventoryCompensate # 定义消费者函数
stream:
bindings:
checkInventory-in-0: # 输入绑定
destination: order.created.topic
group: inventory-service-group
handleInventoryCompensate-in-0:
destination: payment.failed.topic # 监听支付失败,触发回滚
2. 订单服务(发起者 & 最终回滚)
@Service
public class OrderService {
@Autowired
private StreamBridge streamBridge; // Spring Cloud Stream 3.x+ 发送消息推荐方式
@Transactional
public void createOrder(Order order) {
// 1. 本地落库:状态为 PENDING
orderRepository.save(order);
// 2. 发送事件 (注意:这里存在双写风险,生产环境建议配合本地消息表)
streamBridge.send("orderCreated-out-0", new OrderCreatedEvent(order.getId()));
}
// 监听:库存或后续步骤彻底失败后的回滚通知
@Bean
public Consumer<OrderRollbackEvent> handleOrderRollback() {
return event -> {
Order order = orderRepository.findById(event.getOrderId());
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
log.info("订单 {} 已取消", event.getOrderId());
};
}
}
3. 库存服务(中间步骤 & 补偿逻辑)
@Configuration
public class InventoryConfig {
@Autowired
private InventoryService inventoryService;
@Autowired
private StreamBridge streamBridge;
// 正向流程:监听订单创建 -> 扣减库存
@Bean
public Consumer<OrderCreatedEvent> checkInventory() {
return event -> {
boolean success = inventoryService.deductStock(event.getOrderId());
if (success) {
// 成功:发布库存扣减成功事件,触发支付
streamBridge.send("inventoryDeducted-out-0", new InventoryDeductedEvent(event.getOrderId()));
} else {
// 失败:发布库存扣减失败事件,触发订单服务回滚
streamBridge.send("inventoryFailed-out-0", new OrderRollbackEvent(event.getOrderId()));
}
};
}
// 补偿流程:监听支付失败 -> 恢复库存
@Bean
public Consumer<PaymentFailedEvent> handleInventoryCompensate() {
return event -> {
log.info("收到支付失败事件,开始回滚库存:{}", event.getOrderId());
inventoryService.restoreStock(event.getOrderId());
// 继续向上传递回滚信号(触发订单取消)
streamBridge.send("inventoryCompensated-out-0", new OrderRollbackEvent(event.getOrderId()));
};
}
}
4. 支付服务(失败触发点)
@Configuration
public class PaymentConfig {
@Autowired
private StreamBridge streamBridge;
@Bean
public Consumer<InventoryDeductedEvent> processPayment() {
return event -> {
try {
// 模拟业务逻辑
doPayment(event.getOrderId());
streamBridge.send("paymentSuccess-out-0", new PaymentSuccessEvent(event.getOrderId()));
} catch (Exception e) {
// 异常:触发回滚流程
log.error("支付失败,触发Saga回滚链");
streamBridge.send("paymentFailed-out-0", new PaymentFailedEvent(event.getOrderId()));
}
};
}
}
编排式 Saga 的进阶:解决“内存不可靠”问题
生产级编排式 Saga 的实现方案:
- 状态机引擎:使用数据库记录当前 Saga 的状态(Started, StepA_Done, StepB_Done, Failed, Compensating...)。
- 成熟框架:
- Seata (Saga Mode):阿里巴巴开源,通过 JSON 定义状态机,Seata Server 负责记录日志和驱动回滚,非常适合 Spring Cloud 生态。
- Axon Framework:Java 领域成熟的 CQRS/Event Sourcing 框架,内置强大的 Saga 管理器。
简单的编排器伪代码(持久化版):
public void executeSaga(String orderId) {
try {
// 1. 创建订单
sagaLogRepository.save(new SagaStep(orderId, "CREATE_ORDER", "STARTED"));
orderService.create();
// 2. 调用库存 (RPC/Feign)
sagaLogRepository.save(new SagaStep(orderId, "DEDUCT_STOCK", "STARTED"));
inventoryClient.deduct();
// 3. 调用支付 (模拟抛出异常)
sagaLogRepository.save(new SagaStep(orderId, "PAYMENT", "STARTED"));
paymentClient.pay();
} catch (Exception e) {
// 读取日志,反向回滚
List<SagaStep> steps = sagaLogRepository.findByOrderId(orderId);
for (int i = steps.size() - 1; i >= 0; i--) {
compensate(steps.get(i));
}
}
}
关键技术难点:可靠性与本地消息表
在 Saga 模式中,最大的挑战不是逻辑,而是通信的可靠性。
问题: 数据库事务提交了,但 MQ 消息发送失败怎么办?或者 MQ 发送成功了,数据库回滚了怎么办?
解决方案:本地消息表(Transactional Outbox)
你需要优化你的数据库设计,将业务数据和事件日志放在同一个数据库事务中提交。
表设计 (saga_event_outbox)
| 字段名 | 类型 | 描述 |
|---|---|---|
id |
BIGINT | 主键 |
transaction_id |
VARCHAR | 关联业务ID(如订单号) |
event_type |
VARCHAR | 事件类型 (OrderCreated, InventoryCompensated) |
payload |
JSON | 事件内容 |
status |
VARCHAR | NEW(待发送), SENT(已发送) |
create_time |
DATETIME | 创建时间 |
工作流程:
- 业务操作:在同一个
@Transactional方法中,既插入业务表(Order表),也插入消息表(Outbox表)。这保证了原子性。 - 异步发送:有一个独立的定时任务(或监听 Binlog),轮询
status='NEW'的记录,发送到 MQ,发送成功后更新为SENT。 - 幂等性:消费端必须实现幂等性(Idempotency),防止重复消息导致的数据错误(例如重复扣款)。
总结
Saga 模式通过“分而治之”和“知错能改(补偿)”的策略,完美解决了微服务长事务问题。
-
选择建议:
- 如果业务流程简单(3个步骤以内),且流程相对固定,使用 协同式(Event-driven),实现简单。
- 如果业务流程复杂、涉及服务多,或者流程经常变化,请务必使用 编排式(Orchestration),并推荐引入 Seata 等成熟框架来降低开发复杂度。
-
核心准则:
- 幂等性:所有的正向操作和补偿操作都必须支持幂等。
- 向前恢复 vs 向后恢复:Saga 不仅支持回滚(向后恢复),有些场景下(如网络抖动)也可以选择重试(向前恢复)。
- 可观测性:一定要为整个 Saga 链路生成一个全局唯一的 Trace ID,否则在分布式日志中查错将是大海捞针。
浙公网安备 33010602011771号