支持回滚的多步骤顺序任务设计模式选型:Saga还是命令模式?
模式选型结论
Saga模式合理性
Saga模式的核心是解决多步操作的最终一致性问题,核心特征为「步骤拆分+失败时逆序补偿前置已完成操作」,和步骤是否部署为微服务没有强制绑定关系。你当前的审核提交流程属于典型的多步本地事务场景,需要保障失败时数据一致性,选用Saga模式完全合理,这类本地执行的Saga也常被称为「本地Saga」,是业内非常成熟的方案。
Command模式定位
Command模式和Saga模式不是二选一的竞争关系,二者解决的问题维度完全互补:
- Saga负责流程编排、执行顺序控制、失败触发回滚的逻辑
- Command模式负责将每个独立步骤封装为自带执行、回滚逻辑的独立对象,解决你现在用
Action无法关联正向操作和对应回滚操作、无法传递上下文的问题
组合使用二者是当前场景的最优解决方案。
现有BaseOrchestrator设计优化
你当前的实现存在几个核心缺陷:
- 仅存储无关联的
Action,无法将正向步骤和对应的回滚步骤绑定,失败时找不到对应要回滚的操作 - 无上下文传递通道,步骤间需要传递的参数(比如支付交易号、会议ID等)无法流转
- 无执行状态追踪,无法识别哪些步骤已经执行完成需要回滚
优化后的核心实现参考如下:
// 定义通用步骤接口,每个步骤自带执行和补偿逻辑 public interface IOrchestrationStep<TContext> { // 正向执行逻辑 Task ExecuteAsync(TContext context, CancellationToken cancellationToken); // 回滚补偿逻辑 Task CompensateAsync(TContext context, CancellationToken cancellationToken); } // 优化后的编排器基类 public abstract class BaseOrchestrator<TContext> : IOrchestrator<TContext> { private readonly List<IOrchestrationStep<TContext>> _allSteps = new(); private readonly Stack<IOrchestrationStep<TContext>> _executedSteps = new(); protected BaseOrchestrator<TContext> AddStep(IOrchestrationStep<TContext> step) { _allSteps.Add(step); return this; } public async Task ExecuteAsync(TContext context, CancellationToken cancellationToken = default) { foreach (var step in _allSteps) { try { await step.ExecuteAsync(context, cancellationToken); // 执行成功的步骤压入栈,后续失败时逆序回滚 _executedSteps.Push(step); } catch (Exception ex) { // 触发补偿回滚 while (_executedSteps.TryPop(out var completedStep)) { await completedStep.CompensateAsync(context, cancellationToken); } throw new OrchestrationFailedException("审核流程执行失败,已回滚所有已完成操作", ex); } } } }
单个步骤的实现示例(以支付步骤为例):
// 流程上下文,用来传递步骤间需要共享的数据 public class ReviewCreationContext { public int UserId { get; set; } public decimal Amount { get; set; } public string PaymentTransactionId { get; set; } public int MeetingId { get; set; } // 其他需要共享的参数 } public class ProcessPaymentStep : IOrchestrationStep<ReviewCreationContext> { private readonly IPaymentService _paymentService; public ProcessPaymentStep(IPaymentService paymentService) { _paymentService = paymentService; } public async Task ExecuteAsync(ReviewCreationContext context, CancellationToken cancellationToken) { var transactionId = await _paymentService.ChargeAsync(context.UserId, context.Amount, cancellationToken); // 支付成功后把交易号存入上下文,供后续步骤和回滚逻辑使用 context.PaymentTransactionId = transactionId; } public async Task CompensateAsync(ReviewCreationContext context, CancellationToken cancellationToken) { if (!string.IsNullOrEmpty(context.PaymentTransactionId)) { await _paymentService.RefundAsync(context.PaymentTransactionId, cancellationToken); } } }
落地注意事项
- 无副作用的步骤(比如校验预约时段是否可用)可以不用实现补偿逻辑,
CompensateAsync留空即可 - 所有步骤的执行和回滚逻辑都要保证幂等,避免重试、重复回滚时产生脏数据
- 最终有效审核记录的插入可以放在最后一步执行,减少中间状态产生无效数据的概率
- 可新增执行日志埋点,记录每步的执行状态、入参出参,方便问题排查
内容的提问来源于stack exchange,提问作者Manas
相关产品推荐
相关产品推荐

