使用 Masstransit中的 Request/Response 与 Courier 功能实现最终一致性
简介
目前的.net 生态中,最终一致性组件的选择一直是一个问题。本地事务表(cap)需要在每个服务的数据库中插入消息表,而且做不了此类事务 比如:创建订单需要 余额满足+库存满足,库存和余额处于两个服务中。masstransit 是我目前主要用的方案。以往一般都用 masstransit 中的 sagas 来实现 最终一致性,但是随着并发的增加必定会对sagas 持久化的数据库造成很大的压力,根据stackoverflow 中的一个回答 我发现了 一个用 Request/Response 与 Courier 功能 实现最终一致性的方案 Demo地址。
Masstransit 中 Resquest/Response 功能
消息DTO
1 2 3 | public class SampleMessageCommand { } |
消费者
1 2 3 4 5 6 7 | public class SampleMessageCommandHandler : IConsumer<SampleMessageCommand> { public async Task Consume(ConsumeContext<SampleMessageCommand> context) { await context.RespondAsync( new SampleMessageCommandResult() { Data = "Sample" }); } } |
返回结果DTO
1 2 3 4 | public class SampleMessageCommandResult { public string Data { get ; set ; } } |
调用方式与注册方式略过,详情请看 官方文档。
本质上使用消息队列实现 Resquest/Response,客户端(生产者)将请求消息发送至指定消息队列并赋予RequestId和ResponseAddress(临时队列 rabbitmq),服务端(消费者)消费消息并把 需要返回的消息放入指定ResponseAddress,客户端收到 Response message 通过匹配 RequestId 找到 指定Request,最后返回信息。
Masstransit 中 Courier 功能
通过有序组合一系列的Activity,得到一个routing slip。每个 activity(忽略 Execute Activities) 都有 Execute 和 Compensate 两个方法。Compensate 用来执撤销 Execute 方法产生的影响(就是回退 Execute 方法)。每个 Activity Execute 最后都会 调用 Completed 方法把 回退所需要的的信息记录在message中,最后持久化到消息队列的某一个消息中。
余额扣减的Activity ,这里的 DeductBalanceModel 是请求扣减的数据模型,DeductBalanceLog 是回退时需要用到的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class DeductBalanceActivity : IActivity<DeductBalanceModel, DeductBalanceLog> { private readonly ILogger<DeductBalanceActivity> logger; public DeductBalanceActivity(ILogger<DeductBalanceActivity> logger) { this .logger = logger; } public async Task<CompensationResult> Compensate(CompensateContext<DeductBalanceLog> context) { logger.LogInformation( "还原余额" ); var log = context.Log; //可以获取 所有execute 完成时保存的信息 //throw new ArgumentException("some things were wrong"); return context.Compensated(); } public async Task<ExecutionResult> Execute(ExecuteContext<DeductBalanceModel> context) { logger.LogInformation( "扣减余额" ); await Task.Delay(100); return context.Completed( new DeductBalanceLog() { Price = 100 }); } } |
扣减库存 Activity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class DeductStockActivity : IActivity<DeductStockModel, DeductStockLog> { private readonly ILogger<DeductStockActivity> logger; public DeductStockActivity(ILogger<DeductStockActivity> logger) { this .logger = logger; } public async Task<CompensationResult> Compensate(CompensateContext<DeductStockLog> context) { var log = context.Log; logger.LogInformation( "还原库存" ); return context.Compensated(); } public async Task<ExecutionResult> Execute(ExecuteContext<DeductStockModel> context) { var argument = context.Arguments; logger.LogInformation( "扣减库存" ); await Task.Delay(100); return context.Completed( new DeductStockLog() { ProductId = argument.ProductId, Amount = 1 }); } } |
生成订单 Execute Activity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class CreateOrderActivity : IExecuteActivity<CreateOrderModel> { private readonly ILogger<CreateOrderActivity> logger; public CreateOrderActivity(ILogger<CreateOrderActivity> logger) { this .logger = logger; } public async Task<ExecutionResult> Execute(ExecuteContext<CreateOrderModel> context) { logger.LogInformation( "创建订单" ); await Task.Delay(100); //throw new CommonActivityExecuteFaildException("当日订单已达到上限"); return context.CompletedWithVariables( new CreateOrderResult { OrderId= "111122" ,Message= "创建订单成功" }); } } |
组装 以上 Activity 生成一个 Routing Slip,这是一个有序的组合,扣减库存=》扣减余额=》生成订单
1 2 3 4 5 6 7 | var builder = new RoutingSlipBuilder(NewId.NextGuid());<br> builder.AddActivity( "DeductStock" , new Uri($ "{configuration[" RabbitmqConfig:HostUri "]}/DeductStock_execute" ), new DeductStockModel { ProductId = request.Message.ProductId }); builder.AddActivity( "DeductBalance" , new Uri($ "{configuration[" RabbitmqConfig:HostUri "]}/DeductBalance_execute" ), new DeductBalanceModel { CustomerId = request.Message.CustomerId, Price = request.Message.Price }); builder.AddActivity( "CreateOrder" , new Uri($ "{configuration[" RabbitmqConfig:HostUri "]}/CreateOrder_execute" ), new CreateOrderModel { Price = request.Message.Price, CustomerId = request.Message.CustomerId, ProductId = request.Message.ProductId });<br> var routingSlip = builder.Build(); |
执行 Routing Slip
1 | await bus.Execute(routingSlip); |
这里是没有任何返回值的,所有activity都是 异步执行,虽然所有的activity可以执行完成或者由于某个Activity执行出错而全部回退。(其实这里有一种更坏的情况就是 Compensate 出错,默认情况下 Masstransit 只会发送一个回退错误的消息,后面讲到创建订单的时候我会把它塞到错误队列里,这样我们可以通过修改 Compensate bug后重新导入到正常队列来修正数据),这个功能完全满足不了 创建订单这个需求,执行 await bus.Execute(routingSlip) 后我们完全不知道订单到底创建成功,还是由于库存或余额不足而失败了(异步)。
还好 routing slip 在执行过程中产生很多消息,比如 RoutingSlipCompleted ,RoutingSlipCompensationFailed ,RoutingSlipActivityCompleted,RoutingSlipActivityFaulted 等,具体文档,我们可以订阅这些事件,再结合Request/Response 实现 创建订单的功能。
)
实现创建订单(库存满足+余额满足)长流程
创建订单 command
/// <summary> /// 长流程 分布式事务 /// </summary> public class CreateOrderCommand { public string ProductId { get; set; } public string CustomerId { get; set; } public int Price { get; set; } }
事务第一步,扣减库存相关 代码
public class DeductStockActivity : IActivity<DeductStockModel, DeductStockLog> { private readonly ILogger<DeductStockActivity> logger; public DeductStockActivity(ILogger<DeductStockActivity> logger) { this.logger = logger; } public async Task<CompensationResult> Compensate(CompensateContext<DeductStockLog> context) { var log = context.Log; logger.LogInformation("还原库存"); return context.Compensated(); } public async Task<ExecutionResult> Execute(ExecuteContext<DeductStockModel> context) { var argument = context.Arguments; logger.LogInformation("扣减库存"); await Task.Delay(100); return context.Completed(new DeductStockLog() { ProductId = argument.ProductId, Amount = 1 }); } } public class DeductStockModel { public string ProductId { get; set; } } public class DeductStockLog { public string ProductId { get; set; } public int Amount { get; set; } }
事务第二步,扣减余额相关代码
public class DeductBalanceActivity : IActivity<DeductBalanceModel, DeductBalanceLog> { private readonly ILogger<DeductBalanceActivity> logger; public DeductBalanceActivity(ILogger<DeductBalanceActivity> logger) { this.logger = logger; } public async Task<CompensationResult> Compensate(CompensateContext<DeductBalanceLog> context) { logger.LogInformation("还原余额"); var log = context.Log; //throw new ArgumentException("some things were wrong"); return context.Compensated(); } public async Task<ExecutionResult> Execute(ExecuteContext<DeductBalanceModel> context) { logger.LogInformation("扣减余额"); await Task.Delay(100); return context.Completed(new DeductBalanceLog() { Price = 100 }); } } public class DeductBalanceModel { public string CustomerId { get; set; } public int Price { get; set; } } public class DeductBalanceLog { public int Price { get; set; } }
事务第三步,创建订单相关代码
public class CreateOrderActivity : IExecuteActivity<CreateOrderModel> { private readonly ILogger<CreateOrderActivity> logger; public CreateOrderActivity(ILogger<CreateOrderActivity> logger) { this.logger = logger; } public async Task<ExecutionResult> Execute(ExecuteContext<CreateOrderModel> context) { logger.LogInformation("创建订单"); await Task.Delay(100); //throw new CommonActivityExecuteFaildException("当日订单已达到上限"); return context.CompletedWithVariables(new CreateOrderResult { OrderId="111122",Message="创建订单成功" }); } } public class CreateOrderModel { public string ProductId { get; set; } public string CustomerId { get; set; } public int Price { get; set; } } public class CreateOrderResult { public string OrderId { get; set; } public string Message { get; set; } }
我通过 消费 创建订单 request,获取 request 的 response 地址与 RequestId,这两个值 返回 response 时需要用到,我把这些信息存到 RoutingSlip中,并且订阅 RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted | RoutingSlipEvents.CompensationFailed 三种事件,当这三种消息出现时 我会根据 事件类别 和RoutingSlip中 之前加入的 (response 地址与 RequestId)生成 Response ,整个过程大概就是这么个意思,没理解可以看demo。这里由于每一个事物所需要用到的 RoutingSlip + Request/Response 步骤都类似 可以抽象一下(模板方法),把Activity 的组装 延迟到派生类去解决,这个代理类Masstransit有 ,但是官方没有顾及到 CompensationFailed 的情况,所以我干脆自己再写一个。
public abstract class RoutingSlipDefaultRequestProxy<TRequest> : IConsumer<TRequest> where TRequest : class { public async Task Consume(ConsumeContext<TRequest> context) { var builder = new RoutingSlipBuilder(NewId.NextGuid()); builder.AddSubscription(context.ReceiveContext.InputAddress, RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted | RoutingSlipEvents.CompensationFailed); builder.AddVariable("RequestId", context.RequestId); builder.AddVariable("ResponseAddress", context.ResponseAddress); builder.AddVariable("FaultAddress", context.FaultAddress); builder.AddVariable("Request", context.Message); await BuildRoutingSlip(builder, context); var routingSlip = builder.Build(); await context.Execute(routingSlip).ConfigureAwait(false); } protected abstract Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext<TRequest> request); }
这个 是派生类 Routing slip 的拼装过程
public class CreateOrderRequestProxy : RoutingSlipDefaultRequestProxy<CreateOrderCommand> { private readonly IConfiguration configuration; public CreateOrderRequestProxy(IConfiguration configuration) { this.configuration = configuration; } protected override Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext<CreateOrderCommand> request) { builder.AddActivity("DeductStock", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/DeductStock_execute"), new DeductStockModel { ProductId = request.Message.ProductId }); builder.AddActivity("DeductBalance", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/DeductBalance_execute"), new DeductBalanceModel { CustomerId = request.Message.CustomerId, Price = request.Message.Price }); builder.AddActivity("CreateOrder", new Uri($"{configuration["RabbitmqConfig:HostUri"]}/CreateOrder_execute"), new CreateOrderModel { Price = request.Message.Price, CustomerId = request.Message.CustomerId, ProductId = request.Message.ProductId }); return Task.CompletedTask; } }
构造response 基类,主要是对三种情况做处理。
public abstract class RoutingSlipDefaultResponseProxy<TRequest, TResponse, TFaultResponse> : IConsumer<RoutingSlipCompensationFailed>, IConsumer<RoutingSlipCompleted>, IConsumer<RoutingSlipFaulted> where TRequest : class where TResponse : class where TFaultResponse : class { public async Task Consume(ConsumeContext<RoutingSlipCompleted> context) { var request = context.Message.GetVariable<TRequest>("Request"); var requestId = context.Message.GetVariable<Guid>("RequestId"); Uri responseAddress = null; if (context.Message.Variables.ContainsKey("ResponseAddress")) responseAddress = context.Message.GetVariable<Uri>("ResponseAddress"); if (responseAddress == null) throw new ArgumentException($"The response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}"); var endpoint = await context.GetResponseEndpoint<TResponse>(responseAddress, requestId).ConfigureAwait(false); var response = await CreateResponseMessage(context, request); await endpoint.Send(response).ConfigureAwait(false); } public async Task Consume(ConsumeContext<RoutingSlipFaulted> context) { var request = context.Message.GetVariable<TRequest>("Request"); var requestId = context.Message.GetVariable<Guid>("RequestId"); Uri faultAddress = null; if (context.Message.Variables.ContainsKey("FaultAddress")) faultAddress = context.Message.GetVariable<Uri>("FaultAddress"); if (faultAddress == null && context.Message.Variables.ContainsKey("ResponseAddress")) faultAddress = context.Message.GetVariable<Uri>("ResponseAddress"); if (faultAddress == null) throw new ArgumentException($"The fault/response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}"); var endpoint = await context.GetFaultEndpoint<TResponse>(faultAddress, requestId).ConfigureAwait(false); var response = await CreateFaultedResponseMessage(context, request, requestId); await endpoint.Send(response).ConfigureAwait(false); } public async Task Consume(ConsumeContext<RoutingSlipCompensationFailed> context) { var request = context.Message.GetVariable<TRequest>("Request"); var requestId = context.Message.GetVariable<Guid>("RequestId"); Uri faultAddress = null; if (context.Message.Variables.ContainsKey("FaultAddress")) faultAddress = context.Message.GetVariable<Uri>("FaultAddress"); if (faultAddress == null && context.Message.Variables.ContainsKey("ResponseAddress")) faultAddress = context.Message.GetVariable<Uri>("ResponseAddress"); if (faultAddress == null) throw new ArgumentException($"The fault/response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}"); var endpoint = await context.GetFaultEndpoint<TResponse>(faultAddress, requestId).ConfigureAwait(false); var response = await CreateCompensationFaultedResponseMessage(context, request, requestId); await endpoint.Send(response).ConfigureAwait(false); } protected abstract Task<TResponse> CreateResponseMessage(ConsumeContext<RoutingSlipCompleted> context, TRequest request); protected abstract Task<TFaultResponse> CreateFaultedResponseMessage(ConsumeContext<RoutingSlipFaulted> context, TRequest request, Guid requestId); protected abstract Task<TFaultResponse> CreateCompensationFaultedResponseMessage(ConsumeContext<RoutingSlipCompensationFailed> context, TRequest request, Guid requestId); }
Response 派生类 ,这里逻辑可以随自己定义,我也是随便写了个 CommonResponse和一个业务错误抛错(牺牲了一点性能)。
public class CreateOrderResponseProxy : RoutingSlipDefaultResponseProxy<CreateOrderCommand, CommonCommandResponse<CreateOrderResult>, CommonCommandResponse<CreateOrderResult>> { protected override Task<CommonCommandResponse<CreateOrderResult>> CreateResponseMessage(ConsumeContext<RoutingSlipCompleted> context, CreateOrderCommand request) { return Task.FromResult(new CommonCommandResponse<CreateOrderResult> { Status = 1, Result = new CreateOrderResult { Message = context.Message.Variables.TryGetAndReturn(nameof(CreateOrderResult.Message))?.ToString(), OrderId = context.Message.Variables.TryGetAndReturn(nameof(CreateOrderResult.OrderId))?.ToString(), } }); } protected override Task<CommonCommandResponse<CreateOrderResult>> CreateFaultedResponseMessage(ConsumeContext<RoutingSlipFaulted> context, CreateOrderCommand request, Guid requestId) { var commonActivityExecuteFaildException = context.Message.ActivityExceptions.FirstOrDefault(m => m.ExceptionInfo.ExceptionType == typeof(CommonActivityExecuteFaildException).FullName); if (commonActivityExecuteFaildException != null) { return Task.FromResult(new CommonCommandResponse<CreateOrderResult> { Status = 2, Message = commonActivityExecuteFaildException.ExceptionInfo.Message }); } // system error log here return Task.FromResult(new CommonCommandResponse<CreateOrderResult> { Status = 3, Message = "System error" }); } protected override Task<CommonCommandResponse<CreateOrderResult>> CreateCompensationFaultedResponseMessage(ConsumeContext<RoutingSlipCompensationFailed> context, CreateOrderCommand request, Guid requestId) { var exception = context.Message.ExceptionInfo; // lg here context.Message.ExceptionInfo return Task.FromResult(new CommonCommandResponse<CreateOrderResult> { Status = 3, Message = "System error" }); } }
对于 CompensationFailed 的处理 通过 ActivityCompensateErrorTransportFilter 实现 发送到错误消息队列,后续通过prometheus + rabbitmq-exporter + alertmanager 触发告警 通知相关人员处理。
public class ActivityCompensateErrorTransportFilter<TActivity, TLog> : IFilter<CompensateActivityContext<TActivity, TLog>> where TActivity : class, ICompensateActivity<TLog> where TLog : class { public void Probe(ProbeContext context) { context.CreateFilterScope("moveFault"); } public async Task Send(CompensateActivityContext<TActivity, TLog> context, IPipe<CompensateActivityContext<TActivity, TLog>> next) { try { await next.Send(context).ConfigureAwait(false); } catch(Exception ex) { if (!context.TryGetPayload(out IErrorTransport transport)) throw new TransportException(context.ReceiveContext.InputAddress, $"The {nameof(IErrorTransport)} was not available on the {nameof(ReceiveContext)}."); var exceptionReceiveContext = new RescueExceptionReceiveContext(context.ReceiveContext, ex); await transport.Send(exceptionReceiveContext); } } }
注册 filter
public class RoutingSlipCompensateErrorSpecification<TActivity, TLog> : IPipeSpecification<CompensateActivityContext<TActivity, TLog>> where TActivity : class, ICompensateActivity<TLog> where TLog : class { public void Apply(IPipeBuilder<CompensateActivityContext<TActivity, TLog>> builder) { builder.AddFilter(new ActivityCompensateErrorTransportFilter<TActivity, TLog>()); } public IEnumerable<ValidationResult> Validate() { yield return this.Success("success"); } } cfg.ReceiveEndpoint("DeductStock_compensate", ep => { ep.PrefetchCount = 100; ep.CompensateActivityHost<DeductStockActivity, DeductStockLog>(context.Container, conf => { conf.AddPipeSpecification(new RoutingSlipCompensateErrorSpecification<DeductStockActivity, DeductStockLog>()); }); });
实现创建产品(创建完成+添加库存)
实现了 创建订单的功能,整个流程其实是同步的,我在想能不能实现最为简单的最终一致性 比如 创建一个产品 ,然后异步生成它的库存 ,我发现是可以的,因为我们可以监听到每一个Execute Activity 的完成事件,并且把出错时的信息通过 filter 塞到 错误队列中。
这里的代码就不贴了,详情请看 demo
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!