使用 Masstransit中的 Request/Response 与 Courier 功能实现最终一致性

简介

  目前的.net 生态中,最终一致性组件的选择一直是一个问题。本地事务表(cap)需要在每个服务的数据库中插入消息表,而且做不了此类事务 比如:创建订单需要 余额满足+库存满足,库存和余额处于两个服务中。masstransit 是我目前主要用的方案。以往一般都用 masstransit 中的 sagas 来实现 最终一致性,但是随着并发的增加必定会对sagas 持久化的数据库造成很大的压力,根据stackoverflow 中的一个回答 我发现了 一个用  Request/Response 与 Courier 功能 实现最终一致性的方案 Demo地址。

Masstransit 中 Resquest/Response 功能 

 消息DTO

    public class SampleMessageCommand
    {
    }

 消费者

    public class SampleMessageCommandHandler : IConsumer
    {
        public async Task Consume(ConsumeContext context)
        {
            await context.RespondAsync(new SampleMessageCommandResult() { Data = "Sample" });
        }
    }

 返回结果DTO

 

    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 是回退时需要用到的信息。

public class DeductBalanceActivity : IActivity
    {
        private readonly ILogger logger;
        public DeductBalanceActivity(ILogger logger)
        {
            this.logger = logger;
        }
        public async Task Compensate(CompensateContext context)
        {
            logger.LogInformation("还原余额");
            var log = context.Log; //可以获取 所有execute 完成时保存的信息
            //throw new ArgumentException("some things were wrong");
            return context.Compensated();
        }

        public async Task Execute(ExecuteContext context)
        {

            logger.LogInformation("扣减余额");
            await Task.Delay(100);
            return context.Completed(new DeductBalanceLog() { Price = 100 });
        }
    }

 

      扣减库存 Activity

    public class DeductStockActivity : IActivity
    {
        private readonly ILogger logger;
        public DeductStockActivity(ILogger logger)
        {
            this.logger = logger;
        }
        public async Task Compensate(CompensateContext context)
        {
            var log = context.Log;
            logger.LogInformation("还原库存");
            return context.Compensated();
        }

        public async Task Execute(ExecuteContext context)
        {
            var argument = context.Arguments;
            logger.LogInformation("扣减库存");
            await Task.Delay(100);
            return context.Completed(new DeductStockLog() { ProductId = argument.ProductId, Amount = 1 });
        }
    }

       生成订单 Execute Activity

    public class CreateOrderActivity : IExecuteActivity
    {
        private readonly ILogger logger;
        public CreateOrderActivity(ILogger logger)
        {
            this.logger = logger;
        }
        public async Task Execute(ExecuteContext context)
        {
            logger.LogInformation("创建订单");
            await Task.Delay(100);
            //throw new CommonActivityExecuteFaildException("当日订单已达到上限");
            return context.CompletedWithVariables(new CreateOrderResult { OrderId="111122",Message="创建订单成功" });
        }
    }

  组装 以上 Activity 生成一个 Routing Slip,这是一个有序的组合,扣减库存=》扣减余额=》生成订单

            var builder = new RoutingSlipBuilder(NewId.NextGuid());
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 });
var routingSlip = builder.Build();

  执行 Routing Slip

await bus.Execute(routingSlip);

  

      这里是没有任何返回值的,所有activity都是 异步执行,虽然所有的activity可以执行完成或者由于某个Activity执行出错而全部回退。(其实这里有一种更坏的情况就是 Compensate 出错,默认情况下 Masstransit 只会发送一个回退错误的消息,后面讲到创建订单的时候我会把它塞到错误队列里,这样我们可以通过修改 Compensate bug后重新导入到正常队列来修正数据),这个功能完全满足不了 创建订单这个需求,执行 await bus.Execute(routingSlip) 后我们完全不知道订单到底创建成功,还是由于库存或余额不足而失败了(异步)。

     还好 routing slip 在执行过程中产生很多消息,比如 RoutingSlipCompleted ,RoutingSlipCompensationFailed ,RoutingSlipActivityCompleted,RoutingSlipActivityFaulted 等,具体文档,我们可以订阅这些事件,再结合Request/Response 实现 创建订单的功能。

实现创建订单(库存满足+余额满足)长流程

创建订单 command 

    /// 
    /// 长流程 分布式事务
    /// 
    public class CreateOrderCommand
    {
        public string ProductId { get; set; }
        public string CustomerId { get; set; }
        public int Price { get; set; }
    }

  事务第一步,扣减库存相关 代码

  public class DeductStockActivity : IActivity
    {
        private readonly ILogger logger;
        public DeductStockActivity(ILogger logger)
        {
            this.logger = logger;
        }
        public async Task Compensate(CompensateContext context)
        {
            var log = context.Log;
            logger.LogInformation("还原库存");
            return context.Compensated();
        }

        public async Task Execute(ExecuteContext 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
    {
        private readonly ILogger logger;
        public DeductBalanceActivity(ILogger logger)
        {
            this.logger = logger;
        }
        public async Task Compensate(CompensateContext context)
        {
            logger.LogInformation("还原余额");
            var log = context.Log;
            //throw new ArgumentException("some things were wrong");
            return context.Compensated();
        }

        public async Task Execute(ExecuteContext 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
    {
        private readonly ILogger logger;
        public CreateOrderActivity(ILogger logger)
        {
            this.logger = logger;
        }
        public async Task Execute(ExecuteContext 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 :
        IConsumer
        where TRequest : class
    {
        public async Task Consume(ConsumeContext 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 request);
    }


 这个 是派生类 Routing slip 的拼装过程 

    public class CreateOrderRequestProxy : RoutingSlipDefaultRequestProxy

    {
        private readonly IConfiguration configuration;
        public CreateOrderRequestProxy(IConfiguration configuration)
        {
            this.configuration = configuration;
        }
        protected override Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext 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 : IConsumer, IConsumer,
        IConsumer
        where TRequest : class
        where TResponse : class
        where TFaultResponse : class
    {
        public async Task Consume(ConsumeContext context)
        {
            var request = context.Message.GetVariable("Request");
            var requestId = context.Message.GetVariable("RequestId");

            Uri responseAddress = null;
            if (context.Message.Variables.ContainsKey("ResponseAddress"))
                responseAddress = context.Message.GetVariable("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(responseAddress, requestId).ConfigureAwait(false);

            var response = await CreateResponseMessage(context, request);

            await endpoint.Send(response).ConfigureAwait(false);
        }

        public async Task Consume(ConsumeContext context)
        {
            var request = context.Message.GetVariable("Request");
            var requestId = context.Message.GetVariable("RequestId");

            Uri faultAddress = null;
            if (context.Message.Variables.ContainsKey("FaultAddress"))
                faultAddress = context.Message.GetVariable("FaultAddress");
            if (faultAddress == null && context.Message.Variables.ContainsKey("ResponseAddress"))
                faultAddress = context.Message.GetVariable("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(faultAddress, requestId).ConfigureAwait(false);

            var response = await CreateFaultedResponseMessage(context, request, requestId);

            await endpoint.Send(response).ConfigureAwait(false);
        }
        public async Task Consume(ConsumeContext context)
        {
            var request = context.Message.GetVariable("Request");
            var requestId = context.Message.GetVariable("RequestId");

            Uri faultAddress = null;
            if (context.Message.Variables.ContainsKey("FaultAddress"))
                faultAddress = context.Message.GetVariable("FaultAddress");
            if (faultAddress == null && context.Message.Variables.ContainsKey("ResponseAddress"))
                faultAddress = context.Message.GetVariable("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(faultAddress, requestId).ConfigureAwait(false);

            var response = await CreateCompensationFaultedResponseMessage(context, request, requestId);

            await endpoint.Send(response).ConfigureAwait(false);
        }
        protected abstract Task CreateResponseMessage(ConsumeContext context, TRequest request);

        protected abstract Task CreateFaultedResponseMessage(ConsumeContext context, TRequest request, Guid requestId);
        protected abstract Task CreateCompensationFaultedResponseMessage(ConsumeContext context, TRequest request, Guid requestId);
    }

 Response 派生类 ,这里逻辑可以随自己定义,我也是随便写了个 CommonResponse和一个业务错误抛错(牺牲了一点性能)。

    public class CreateOrderResponseProxy :
            RoutingSlipDefaultResponseProxy, CommonCommandResponse>
    {

        protected override Task> CreateResponseMessage(ConsumeContext context, CreateOrderCommand request)
        {

            return Task.FromResult(new CommonCommandResponse
            {
                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> CreateFaultedResponseMessage(ConsumeContext 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
                {
                    Status = 2,
                    Message = commonActivityExecuteFaildException.ExceptionInfo.Message
                });
            }
            // system error  log here
            return Task.FromResult(new CommonCommandResponse
            {
                Status = 3,
                Message = "System error"
            });
        }

        protected override Task> CreateCompensationFaultedResponseMessage(ConsumeContext context, CreateOrderCommand request, Guid requestId)
        {
            var exception = context.Message.ExceptionInfo;
            // lg here context.Message.ExceptionInfo
            return Task.FromResult(new CommonCommandResponse
            {
                Status = 3,
                Message = "System error"
            });           
        }
    }

对于  CompensationFailed 的处理 通过 ActivityCompensateErrorTransportFilter 实现 发送到错误消息队列,后续通过prometheus + rabbitmq-exporter + alertmanager 触发告警 通知相关人员处理。

  public class ActivityCompensateErrorTransportFilter : IFilter>
        where TActivity : class, ICompensateActivity
        where TLog : class
    {
        public void Probe(ProbeContext context)
        {
            context.CreateFilterScope("moveFault");
        }

        public async Task Send(CompensateActivityContext context, IPipe> 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 : IPipeSpecification>
        where TActivity : class, ICompensateActivity
        where TLog : class
    {
        public void Apply(IPipeBuilder> builder)
        {
            builder.AddFilter(new ActivityCompensateErrorTransportFilter());
        }

        public IEnumerable Validate()
        {
           yield return this.Success("success");
        }
    }


            cfg.ReceiveEndpoint("DeductStock_compensate", ep =>
            {
                ep.PrefetchCount = 100;
                ep.CompensateActivityHost(context.Container, conf =>
                 {
                     conf.AddPipeSpecification(new RoutingSlipCompensateErrorSpecification());
                 });

            });

 

实现创建产品(创建完成+添加库存)

实现了 创建订单的功能,整个流程其实是同步的,我在想能不能实现最为简单的最终一致性 比如 创建一个产品 ,然后异步生成它的库存 ,我发现是可以的,因为我们可以监听到每一个Execute Activity 的完成事件,并且把出错时的信息通过 filter 塞到 错误队列中。

这里的代码就不贴了,详情请看 demo

 

你可能感兴趣的:(使用 Masstransit中的 Request/Response 与 Courier 功能实现最终一致性)