热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

MassTransit知多少|基于StateMachine实现Saga编排式分布式事务

原标题:MassTransit知多少|基于StateMachine实现Saga编排式分布式事务什么是状态机状态机作为一种程序开发范例

原标题:MassTransit 知多少 | 基于StateMachine实现Saga编排式分布式事务

什么是状态机

状态机作为一种程序开发范例,在实际的应用开发中有很多的应用场景,其中.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.OrderServiceASP.NET Core Web API
库存服务MassTransit.SmDemo.InventoryServiceWorker Service
支付服务MassTransit.SmDemo.PaymentServiceWorker Service
共享类库MassTransit.SmDemo.SharedClass 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 CreateOrder(CreateOrderDto createOrderDto)
{
await _bus.Publish(new
{
createOrderDto.CustomerId,
createOrderDto.ShoppingCartItems
});
return Ok();
}
}

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

public class CreateOrderConsumer : IConsumer
{
private readonly ILogger _logger;
public CreateOrderConsumer(ILogger logger)
{
_logger = logger;
}
public async Task Consume(ConsumeContext 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 {owww.yii666.comrder.OrderId} created successfully");
await context.Publish(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 OrderItems { get; set; }
}


  1. 定义状态机:直接继承自MassTransitStateMachine并同时指定状态实例即可:

namespace MassTransit.SmDemo.OrderService;
public class OrderStateMachine : MassTransitStateMachine
{
}


  1. 注册状态机:这里指定内存持久化方式来持久化状态,也可指定诸如MongoDb、MySQL等数据库进行状态持久化:

return services.AddMassTransit(x =>
{
//...
x.AddSagaStateMachine()
.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类型定义,本例涉及有:

    1. 订单成功创建事件:public Event OrderCreated {get; private set;}

    2. 库存扣减成功事件:public Event DeduceInventorySucceed {get; private set;}

    3. 库存扣减失败事件:public Event DeduceInventoryFailed {get; private set;}

    4. 订单支付成功事件:public Event PayOrderSucceed {get; private set;}

    5. 订单支付失败事件:public Event PayOrderFailed {get; private set;}

    6. 库存已返还事件:public Event ReturnInventorySucceed { get; private set; }

    7. 订单取消事件:public Event 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(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(new
{
context.Saga.OrderId,
context.Saga.Amount
});
}).TransitionTo(InventoryDeducted),
When(DeduceInventoryFailed).Then(cOntext=>
{
context.Publish(new
{
context.Saga.OrderId
});
})
);

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

During(InventoryDeducted,
When(PayOrderFailed).Then(cOntext=>
{
context.Publish(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
{
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 OrderCreated { get; private set; }
public Event DeduceInventorySucceed { get; private set; }
public Event DeduceInventoryFailed { get; private set; }
public Event OrderCanceled { get; private set; }
public Event PayOrderSucceed { get; private set; }
public Event PayOrderFailed { get; private set; }
public Event ReturnInventorySucceed { get; private set; }
public Event 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.OnM文章来源地址19328.htmlissingInstance(m =>
{
return m.ExecuteAsync(x => x.RespondAsync(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(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(new
{
contex文章来源地址19328.htmlt.Saga.OrderId,
context.Saga.Amount
});
}).TransitionTo(InventoryDeducted),
When(DeduceInventoryFailed).Then(cOntext=>
{
context.Publish(new
{
context.Saga.OrderId
www.yii666.com });
})
);
During(InventoryDeducted,
When(PayOrderFailed).Then(cOntext=>
{
context.Publish(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(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(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
{
private readonly ILogger _logger;
public DeduceInventoryConsumer(ILogger logger)
{
_logger = logger;
}
public async Task Consume(ConsumeContext context)
{
if (!CheckStock(context.Message.DeduceInventoryItems))
{
_logger.LogWarning($"Insufficient stock for order [{context.Message.OrderId}]!");
await context.Publish(
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(new { context.Message.OrderId });
}
}
private bool CheckStock(List deduceItems)
{
foreach (var stockItem in deduceItems)
{
if (InventoryRepository.GetStock(stockItem.SkuId) }
return true;
}
private void DeduceStocks(List deduceItems)
{
foreach (var stockItem in deduceItems)
文章来源站点https://www.yii666.com/ {
InventoryRepository.TryDeduceStock(stockItem.SkuId, stockItem.Qty);
}
}
}

namespace MassTransit.SmDemo.InventoryService.Consumers;
public class ReturnInventoryConsumer : IConsumer
{
private readonly ILogger _logger;
public ReturnInventoryConsumer(ILogger logger)
{
_logger = logger;
}
public async Task Consume(ConsumeContext 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(new { context.Message.OrderId });
}
}

支付服务

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

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

运行结果

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

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

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

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

总结

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

来源于:MassTransit 知多少 | 基于StateMachine实现Saga编排式分布式事务


推荐阅读
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • [翻译]微服务设计模式5. 服务发现服务端服务发现
    服务之间需要互相调用,在单体架构中,服务之间的互相调用直接通过编程语言层面的方法调用就搞定了。在传统的分布式应用的部署中,服务地 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • PHP中的单例模式与静态变量的区别及使用方法
    本文介绍了PHP中的单例模式与静态变量的区别及使用方法。在PHP中,静态变量的存活周期仅仅是每次PHP的会话周期,与Java、C++不同。静态变量在PHP中的作用域仅限于当前文件内,在函数或类中可以传递变量。本文还通过示例代码解释了静态变量在函数和类中的使用方法,并说明了静态变量的生命周期与结构体的生命周期相关联。同时,本文还介绍了静态变量在类中的使用方法,并通过示例代码展示了如何在类中使用静态变量。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文介绍了一种解析GRE报文长度的方法,通过分析GRE报文头中的标志位来计算报文长度。具体实现步骤包括获取GRE报文头指针、提取标志位、计算报文长度等。该方法可以帮助用户准确地获取GRE报文的长度信息。 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • Python中的PyInputPlus模块原文:https ... [详细]
  • 由于同源策略的限制,满足同源的脚本才可以获取资源。虽然这样有助于保障网络安全,但另一方面也限制了资源的使用。那么如何实现跨域呢,以下是实现跨域的一些方法。 ... [详细]
author-avatar
紫藤老君的八卦炉
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有