什么是状态机

状态机作为一种程序开发范例,在实际的应用开发中有很多的应用场景,其中.NET 中的async/await 的核心底层实现就是基于状态机机制。状态机分为两种:有限状态机和无限状态机,本文介绍的就是有限状态机,有限状态机在任何时候都可以准确地处于有限状态中的一种,其可以根据一些输入从一个状态转换到另一个状态。一个有限状态机是由其状态列表、初始状态和触发每个转换的输入来定义的。如下图展示的就是一个闸机的状态机示意图:

从上图可以看出,状态机主要有以下核心概念:

  1. State:状态,闸机有已开启(opened)和已关闭(closed)状态。
  2. Transition:转移,即闸机从一个状态转移到另一个状态的过程。
  3. Transition Condition:转移条件,也可理解为事件,即闸机在某一状态下只有触发了某个转移条件,才会执行状态转移。比如,闸机处于已关闭状态时,只有接收到开启事件才会执行转移动作,进而转移到开启状态。
  4. Action:动作,即完成状态转移要执行的动作。比如要从关闭状态转移到开启状态,则需要执行开闸动作。

在.NET中,dotnet-state-machine/statelessMassTransit都提供了开箱即用的状态机实现。本文将重点介绍MassTransit中的状态机在Saga 模式中的应用。

MassTransit StateMachine

在MassTransit 中MassTransitStateMachine就是状态机的具体抽象,可以用其编排一系列事件来实现状态的流转,也可以用来实现Saga模式的分布式事务。并支持与EF Core和Dapper集成将状态持久化到关系型数据库,也支持将状态持久化到MongoDB、Redis等数据库。是以简单的下单流程:创建订单->扣减库存->支付订单举例而言,其示意图如下所示。

基于状态机实现编排式Saga事务

那具体如何使用MassTransitStateMachine来应用编排式Saga 模式呢,接下来就来创建解决方案来实现以上下单流程示例。依次创建以下项目,除共享类库项目外,均安装MassTransitMassTransit.RabbitMQNuGet包。

项目 项目名 项目类型
订单服务 MassTransit.SmDemo.OrderService ASP.NET Core Web API
库存服务 MassTransit.SmDemo.InventoryService Worker Service
支付服务 MassTransit.SmDemo.PaymentService Worker Service
共享类库 MassTransit.SmDemo.Shared Class Library

三个服务都添加扩展类MassTransitServiceExtensions,并在Program.cs类中调用services.AddMassTransitWithRabbitMq();注册服务。

using System.Reflection;
using MassTransit.CourierDemo.Shared.Models; namespace MassTransit.CourierDemo.InventoryService; public static class MassTransitServiceExtensions
{
public static IServiceCollection AddMassTransitWithRabbitMq(this IServiceCollection services)
{
return services.AddMassTransit(x =>
{
x.SetKebabCaseEndpointNameFormatter(); // By default, sagas are in-memory, but should be changed to a durable
// saga repository.
x.SetInMemorySagaRepositoryProvider(); var entryAssembly = Assembly.GetEntryAssembly();
x.AddConsumers(entryAssembly);
x.AddSagaStateMachines(entryAssembly);
x.AddSagas(entryAssembly);
x.AddActivities(entryAssembly);
x.UsingRabbitMq((context, busConfig) =>
{
busConfig.Host(
host: "localhost",
port: 5672,
virtualHost: "masstransit",
configure: hostConfig =>
{
hostConfig.Username("guest");
hostConfig.Password("guest");
}); busConfig.ConfigureEndpoints(context);
});
});
}
}

订单服务

订单服务作为下单流程中的核心服务,主要职责包含接收创建订单请求和订单状态机的实现。先来定义OrderController如下:

namespace MassTransit.SmDemo.OrderService.Controllers;
[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
private readonly IBus _bus;
public OrderController(IBus bus)
{
_bus = bus;
} [HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderDto createOrderDto)
{
await _bus.Publish<ICreateOrderCommand>(new
{
createOrderDto.CustomerId,
createOrderDto.ShoppingCartItems
});
return Ok();
}
}

紧接着,订阅ICreateOrderCommand,执行订单创建逻辑,订单创建完毕后会发布ICreateOrderSucceed事件。

public class CreateOrderConsumer : IConsumer<ICreateOrderCommand>
{
private readonly ILogger<CreateOrderConsumer> _logger; public CreateOrderConsumer(ILogger<CreateOrderConsumer> logger)
{
_logger = logger;
}
public async Task Consume(ConsumeContext<ICreateOrderCommand> context)
{
var shoppingItems =
context.Message.ShoppingCartItems.Select(item => new ShoppingCartItem(item.SkuId, item.Price, item.Qty));
var order = new Order(context.Message.CustomerId).NewOrder(shoppingItems.ToArray());
await OrderRepository.Insert(order); _logger.LogInformation($"Order {order.OrderId} created successfully");
await context.Publish<ICreateOrderSucceed>(new
{
order.OrderId,
order.OrderItems
});
}
}

最后来实现订单状态机,主要包含以下几步:

  1. 定义状态机状态: 一个状态机从启动到结束可能会经历各种异常,包括程序异常或物理故障,为确保状态机能从异常中恢复,因此必须保存状态机的状态。本例中,定义OrderState以保存状态机实例状态数据:
using MassTransit.SmDemo.OrderService.Domains;

namespace MassTransit.SmDemo.OrderService;

public class OrderState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public Guid OrderId { get; set; }
public decimal Amount { get; set; }
public List<OrderItem> OrderItems { get; set; }
}
  1. 定义状态机:直接继承自MassTransitStateMachine并同时指定状态实例即可:
namespace MassTransit.SmDemo.OrderService;

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
}
  1. 注册状态机:这里指定内存持久化方式来持久化状态,也可指定诸如MongoDb、MySQL等数据库进行状态持久化:
return services.AddMassTransit(x =>
{
//...
x.AddSagaStateMachine<OrderStateMachine, OrderState>()
.InMemoryRepository();
}
  1. 定义状态列表:即状态机涉及到的系列状态,并通过State类型定义,本例中为:

    1. 已创建:public State Created { get; private set; }
    2. 库存已扣减:public State InventoryDeducted { get; private set; }
    3. 已支付:public State Paid { get; private set; }
    4. 已取消:public State Canceled { get; private set; }
  2. 定义转移条件:即推动状态流转的事件,通过Event<T>类型定义,本例涉及有:
    1. 订单成功创建事件:public Event<ICreateOrderSucceed> OrderCreated {get; private set;}
    2. 库存扣减成功事件:public Event<IDeduceInventorySucceed> DeduceInventorySucceed {get; private set;}
    3. 库存扣减失败事件:public Event<IDeduceInventoryFailed> DeduceInventoryFailed {get; private set;}
    4. 订单支付成功事件:public Event<IPayOrderSucceed> PayOrderSucceed {get; private set;}
    5. 订单支付失败事件:public Event<IPayOrderFailed> PayOrderFailed {get; private set;}
    6. 库存已返还事件:public Event<IReturnInventorySucceed> ReturnInventorySucceed { get; private set; }
    7. 订单取消事件:public Event<ICancelOrderSucceed> OrderCanceled { get; private set; }
  3. 定义关联关系:由于每个事件都是孤立的,但相关联的事件终会作用到某个具体的状态机实例上,如何关联事件以推动状态机的转移呢?配置关联Id。以下就是将事件消息中的传递的OrderId作为关联ID。
    1. Event(() => OrderCreated, x => x.CorrelateById(m => m.Message.OrderId));
    2. Event(() => DeduceInventorySucceed, x => x.CorrelateById(m => m.Message.OrderId));
    3. Event(() => DeduceInventoryFailed, x => x.CorrelateById(m => m.Message.OrderId));
    4. Event(() => PayOrderSucceed, x => x.CorrelateById(m => m.Message.OrderId));
  4. 定义状态转移:即状态在什么条件下做怎样的动作完成状态的转移,本例中涉及的正向状态转移有:

(1) 初始状态->已创建:触发条件为OrderCreated事件,同时要发送IDeduceInventoryCommand推动库存服务执行库存扣减。

Initially(
When(OrderCreated)
.Then(context =>
{
context.Saga.OrderId = context.Message.OrderId;
context.Saga.OrderItems = context.Message.OrderItems;
context.Saga.Amount = context.Message.OrderItems.Sum(x => x.Price * x.Qty);
})
.PublishAsync(context => context.Init<IDeduceInventoryCommand>(new
{
context.Saga.OrderId,
DeduceInventoryItems =
context.Saga.OrderItems.Select(x => new DeduceInventoryItem(x.SkuId, x.Qty)).ToList()
}))
.TransitionTo(Created));

(2) 已创建-> 库存已扣减:触发条件为DeduceInventorySucceed事件,同时要发送IPayOrderCommand推动支付服务执行订单支付。

During(Created,
When(DeduceInventorySucceed)
.Then(context =>
{
context.Publish<IPayOrderCommand>(new
{
context.Saga.OrderId,
context.Saga.Amount
});
}).TransitionTo(InventoryDeducted),
When(DeduceInventoryFailed).Then(context =>
{
context.Publish<ICancelOrderCommand>(new
{
context.Saga.OrderId
});
})
);

(3) 库存已扣减->已支付:触发条件为PayOrderSucceed事件,转移到已支付后,流程结束。

During(InventoryDeducted,
When(PayOrderFailed).Then(context =>
{
context.Publish<IReturnInventoryCommand>(new
{
context.Message.OrderId,
ReturnInventoryItems =
context.Saga.OrderItems.Select(x => new ReturnInventoryItem(x.SkuId, x.Qty)).ToList()
});
}),
When(PayOrderSucceed).TransitionTo(Paid).Then(context => context.SetCompleted()));

最终完整版的OrderStateMachine如下所示:

using MassTransit.SmDemo.OrderService.Events;
using MassTransit.SmDemo.Shared.Contracts; namespace MassTransit.SmDemo.OrderService; public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
public State Created { get; private set; }
public State InventoryDeducted { get; private set; }
public State Paid { get; private set; }
public State Canceled { get; private set; } public Event<ICreateOrderSucceed> OrderCreated { get; private set; }
public Event<IDeduceInventorySucceed> DeduceInventorySucceed { get; private set; }
public Event<IDeduceInventoryFailed> DeduceInventoryFailed { get; private set; }
public Event<ICancelOrderSucceed> OrderCanceled { get; private set; }
public Event<IPayOrderSucceed> PayOrderSucceed { get; private set; }
public Event<IPayOrderFailed> PayOrderFailed { get; private set; }
public Event<IReturnInventorySucceed> ReturnInventorySucceed { get; private set; }
public Event<IOrderStateRequest> OrderStateRequested { get; private set; } public OrderStateMachine()
{
Event(() => OrderCreated, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => DeduceInventorySucceed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => DeduceInventoryFailed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => ReturnInventorySucceed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PayOrderSucceed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => PayOrderFailed, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => OrderCanceled, x => x.CorrelateById(m => m.Message.OrderId));
Event(() => OrderStateRequested, x =>
{
x.CorrelateById(m => m.Message.OrderId);
x.OnMissingInstance(m =>
{
return m.ExecuteAsync(x => x.RespondAsync<IOrderNotFoundOrCompleted>(new { x.Message.OrderId }));
});
}); InstanceState(x => x.CurrentState); Initially(
When(OrderCreated)
.Then(context =>
{
context.Saga.OrderId = context.Message.OrderId;
context.Saga.OrderItems = context.Message.OrderItems;
var amount = context.Message.OrderItems.Sum(x => x.Price * x.Qty);
context.Saga.Amount = amount;
})
.PublishAsync(context => context.Init<IDeduceInventoryCommand>(new
{
context.Saga.OrderId,
DeduceInventoryItems =
context.Saga.OrderItems.Select(x => new DeduceInventoryItem(x.SkuId, x.Qty)).ToList()
}))
.TransitionTo(Created)); During(Created,
When(DeduceInventorySucceed)
.Then(context =>
{
context.Publish<IPayOrderCommand>(new
{
context.Saga.OrderId,
context.Saga.Amount
});
}).TransitionTo(InventoryDeducted),
When(DeduceInventoryFailed).Then(context =>
{
context.Publish<ICancelOrderCommand>(new
{
context.Saga.OrderId
});
})
); During(InventoryDeducted,
When(PayOrderFailed).Then(context =>
{
context.Publish<IReturnInventoryCommand>(new
{
context.Message.OrderId,
ReturnInventoryItems =
context.Saga.OrderItems.Select(x => new ReturnInventoryItem(x.SkuId, x.Qty)).ToList()
});
}),
When(PayOrderSucceed).TransitionTo(Paid).Then(context => context.SetCompleted()),
When(ReturnInventorySucceed)
.ThenAsync(context => context.Publish<ICancelOrderCommand>(new
{
context.Saga.OrderId
})).TransitionTo(Created)); DuringAny(When(OrderCanceled).TransitionTo(Canceled).ThenAsync(async context =>
{
await Task.Delay(TimeSpan.FromSeconds(10));
await context.SetCompleted();
})); DuringAny(
When(OrderStateRequested)
.RespondAsync(x => x.Init<IOrderStateResponse>(new
{
x.Saga.OrderId,
State = x.Saga.CurrentState
}))
);
}
}

库存服务

库存服务在整个下单流程的职责主要是库存的扣减和返还,其仅需要订阅IDeduceInventoryCommandIReturnInventoryCommand两个命令并实现即可。代码如下所示:

using MassTransit.SmDemo.InventoryService.Repositories;
using MassTransit.SmDemo.Shared.Contracts; namespace MassTransit.SmDemo.InventoryService.Consumers; public class DeduceInventoryConsumer : IConsumer<IDeduceInventoryCommand>
{
private readonly ILogger<DeduceInventoryConsumer> _logger; public DeduceInventoryConsumer(ILogger<DeduceInventoryConsumer> logger)
{
_logger = logger;
} public async Task Consume(ConsumeContext<IDeduceInventoryCommand> context)
{
if (!CheckStock(context.Message.DeduceInventoryItems))
{
_logger.LogWarning($"Insufficient stock for order [{context.Message.OrderId}]!");
await context.Publish<IDeduceInventoryFailed>(
new { context.Message.OrderId, Reason = "insufficient stock" });
}
else
{
_logger.LogInformation($"Inventory has been deducted for order [{context.Message.OrderId}]!");
DeduceStocks(context.Message.DeduceInventoryItems);
await context.Publish<IDeduceInventorySucceed>(new { context.Message.OrderId });
}
} private bool CheckStock(List<DeduceInventoryItem> deduceItems)
{
foreach (var stockItem in deduceItems)
{
if (InventoryRepository.GetStock(stockItem.SkuId) < stockItem.Qty) return false;
} return true;
} private void DeduceStocks(List<DeduceInventoryItem> deduceItems)
{
foreach (var stockItem in deduceItems)
{
InventoryRepository.TryDeduceStock(stockItem.SkuId, stockItem.Qty);
}
}
}
namespace MassTransit.SmDemo.InventoryService.Consumers;

public class ReturnInventoryConsumer : IConsumer<IReturnInventoryCommand>
{
private readonly ILogger<ReturnInventoryConsumer> _logger; public ReturnInventoryConsumer(ILogger<ReturnInventoryConsumer> logger)
{
_logger = logger;
} public async Task Consume(ConsumeContext<IReturnInventoryCommand> context)
{
foreach (var returnInventoryItem in context.Message.ReturnInventoryItems)
{
InventoryRepository.ReturnStock(returnInventoryItem.SkuId, returnInventoryItem.Qty);
} _logger.LogInformation($"Inventory has been returned for order [{context.Message.OrderId}]!");
await context.Publish<IReturnInventorySucceed>(new { context.Message.OrderId });
}
}

支付服务

对于下单流程的支付用例来说,要么成功要么失败,因此仅需要订阅IPayOrderCommand命令即可,具体PayOrderConsumer实现如下:

using MassTransit.SmDemo.Shared.Contracts;

namespace MassTransit.SmDemo.PaymentService.Consumers;

public class PayOrderConsumer : IConsumer<IPayOrderCommand>
{
private readonly ILogger<PayOrderConsumer> _logger; public PayOrderConsumer(ILogger<PayOrderConsumer> logger)
{
_logger = logger;
}
public async Task Consume(ConsumeContext<IPayOrderCommand> context)
{
await Task.Delay(TimeSpan.FromSeconds(10));
if (context.Message.Amount % 2 == 0)
{_logger.LogInformation($"Order [{context.Message.OrderId}] paid successfully!");
await context.Publish<IPayOrderSucceed>(new { context.Message.OrderId });
}
else
{
_logger.LogWarning($"Order [{context.Message.OrderId}] payment failed!");
await context.Publish<IPayOrderFailed>(new
{
context.Message.OrderId,
Reason = "Insufficient account balance"
});
}
}
}

运行结果

启动三个项目,并在Swagger中发起订单创建请求,如下图所示:

由于订单总额为奇数,因此支付会失败,最终控制台输出如下图所示:

打开RabbitMQ后台,可以看见MassTransit按照约定创建了以下队列用于服务间的消息传递:

其中order-state队列绑定到类型为fanout的同名order-stateExchange,其绑定关系如下图所示,该Exchange负责从其他同名事件的Exchange转发事件。

总结

通过以上示例的讲解,相信了解到MassTransit StateMachine的强大之处。StateMachine充当着事务编排器的角色,通过集中定义状态、转移条件和状态转移的执行顺序,实现高内聚的事务流转控制,也确保了其他伴生服务仅需关注自己的业务逻辑,而无需关心事务的流转,真正实现了关注点分离。

MassTransit | 基于StateMachine实现Saga编排式分布式事务的更多相关文章

  1. 基于支付系统真实场景的分布式事务解决方案效果演示: http://www.iqiyi.com/w_19rsveqlhh.html

    基于支付系统真实场景的分布式事务解决方案效果演示:http://www.iqiyi.com/w_19rsveqlhh.html

  2. Seata-一站式分布式事务解决方案

    Fescar 2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),和社区一起共建开源分布式事务解决方案. F ...

  3. 如何选择分布式事务形态(TCC,SAGA,2PC,补偿,基于消息最终一致性等等)

    各种形态的分布式事务 分布式事务有多种主流形态,包括: 基于消息实现的分布式事务 基于补偿实现的分布式事务(gts/fescar自动补偿的形式) 基于TCC实现的分布式事务 基于SAGA实现的分布式事 ...

  4. 如何选择分布式事务形态(TCC,SAGA,2PC,基于消息最终一致性等等)

    各种形态的分布式事务 分布式事务有多种主流形态,包括: 基于消息实现的分布式事务 基于补偿实现的分布式事务 基于TCC实现的分布式事务 基于SAGA实现的分布式事务 基于2PC实现的分布式事务 这些形 ...

  5. 分布式事务(Seata) 四大模式详解

    前言 在上一节中我们讲解了,关于分布式事务和seata的基本介绍和使用,感兴趣的小伙伴可以回顾一下<别再说你不知道分布式事务了!> 最后小农也说了,下期会带给大家关于Seata中关于sea ...

  6. 3种使用MQ实现分布式事务的方式

    1.保证消息传递与一致性 1.1生产者确保消息自主性 当生产者发送一条消息时,它必须完成他的所有业务操作. 如下图: 这保证消费者接受到消息时,生产者已处理完毕相关业务,也就是1PC的基础. 1.2 ...

  7. 关于分布式事务、两阶段提交、一阶段提交、Best Efforts 1PC模式和事务补偿机制的研究 转载

    1.XA XA是由X/Open组织提出的分布式事务的规范.XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接 ...

  8. sql server 分布式事务

    使用分布式事务刚好可以解决集群同时更新多台SQL SERVER数据库,要么全部成功,要么全部回滚的需要. 原来微软早考虑到此方面的问题了. 下面背书,贴出微软官网上面的帮助文档: 分布式事务跨越两个或 ...

  9. 分布式事务、XA、两阶段提交、一阶段提交

    本文原文连接:http://blog.csdn.net/bluishglc/article/details/7612811 ,转载请注明出处! 1.XA XA是由X/Open组织提出的分布式事务的规范 ...

  10. 消息服务框架(MSF)应用实例之分布式事务三阶段提交协议的实现

    一,分布式事务简介 在当前互联网,大数据和人工智能的热潮中,传统企业也受到这一潮流的冲击,纷纷响应国家“互联网+”的战略号召,企业开始将越来越多的应用从公司内网迁移到云端和移动端,或者将之前孤立的IT ...

随机推荐

  1. 第一个Spring Boot的MVC程序

    最近在学习Spring Boot,记录一下学习过程!!!! Spring Boot中的MVC:M(model模型),C(controller控制器),V(view视图) model:是Java的实体B ...

  2. 使用request对象进行简单的注册以及信息显示

    Request内置对象的使用 概述:request对象主要用于接收客户端发送的请求信息,客户端的请求信息被封装在request对象中,通过它才能了解到客户的需求,然后做出响应.封装了用户提交的信息.在 ...

  3. Codeforces 1682 D Circular Spanning Tree

    题意 1-n排列,构成一个圆:1-n每个点有个值0或者1,0代表点的度为偶数,1代表点的度为计数:询问能否构成一棵树,树的连边在圆内不会相交,在圆边上可以相交,可以则输出方案. 提示 1. 首先考虑什 ...

  4.  iOS App 上架App Store及提交审核详细教程

    上架App Store审核分7步进行: 1.安装iOS上架辅助软件Appuploader 2.申请iOS发布证书(p12) 3.申请iOS发布描述文件(mobileprovision) 4.打包ipa ...

  5. springboot如何处理矩阵参数类型的url

    矩阵参数类型的url如何处理 首先要开启这个功能 在webconfig类中创建Webconfigurer类 并且设置 urlPathHelper类中的removeSemicolonContent 为f ...

  6. Java 多线程写zip文件遇到的错误 write beyond end of stream!

    最近在写一个大量小文件直接压缩到一个zip的需求,由于zip中的entry每一个都是独立的,不需要追加写入,也就是一个entry文件,写一个内容, 因此直接使用了多线程来处理,结果就翻车了,代码给出了 ...

  7. P6492 STEP(线段树维护左右区间pushup)

    题目链接 题目描述: 给定一个长度为\(~\)n\(~\)的字符序列\(~\)a,初始时序列中全部都是字符\(~\)L. 有\(~\)q\(~\)次修改,每次给定一个\(~\)x,做出如下变化: \( ...

  8. 成熟企业级开源监控解决方案Zabbix6.2关键功能实战-下

    @ 目录 实战 Zabbix server源码安装使用示例 部署 配置 Zabbix agent2使用示例 部署 配置 Zabbix proxy使用示例 部署 配置 自定义监控使用示例 触发器使用示例 ...

  9. 重启redis

    [root@lecode-dev-001 packages]# /usr/local/redis/bin/redis-cli -p 6379 127.0.0.1:6379> auth Redis ...

  10. 2022-11-07 Acwing每日一题

    本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...