APB VNext系列(二):消息队列ReBus
ABP vNext 封装了两种事件总线结构,第一种是 ABP vNext 自己实现的本地事件总线,这种事件总线无法跨项目发布和订阅。第二种则是分布式事件总线,ABP vNext 自己封装了一个抽象层进行定义,并使用 RabbitMQ 编写了一个基本实现。
我们在实际生产中并不能开箱即用,所以集成了Rebus框架,重新开发了消息队列组件,Rebus的相关介绍资料不多,接下来就给大家介绍下我们是如何集成Rebus到ABP VNext框架里的。
首先我们这个一个标准的服务分为两个项目,一个是前端API站点,另外一个是API Work站点,后者只用来接收MQ消息进行消费,不提供对外接口。
1.ReBus简介
Rebus是.NET的精益服务总线实现。它依赖于Newtonsoft JSON.NET,它支持.NET 4.5和.NET Standard 2.0作为平台目标。这意味着无论您使用的是完整的.NET框架还是使用的是.NET Core,您的平台上都非常有可能支持Rebus。
简单来说它屏蔽了消息队列工具内部复杂的配置交互逻辑,对外暴露出一系列相关的直接使用方法以谋求达到接近于开箱即用的效果。
Rebus GitHub:https://github.com/rebus-org/Rebus
2.封装介绍
那如何对其进行改造集成到我们的ABP VNext项目中呢,我来介绍下其内部的核心方法来帮助大家理解其中的消息队列模块。
那我们知道消息队列是分为生产者和消费者的,生产者发布消息,消费者订阅相关队列来接收消息。那两者之间就是通过队列在进行绑定的,所以在项目启动的时间我们首先要做的就是声明绑定队列。
在这里交换器我们使用Rebus默认声明的交换器哈。
我们封装了一个方法来进行队列的声明和绑定,里面就是针对于Rebus提供的相关接口的封装:
public static void UseRabbitMq(this ServiceConfigurationContext context, [NotNull] string queueName) { if (string.IsNullOrEmpty(queueName)) { throw new ArgumentNullException(nameof(queueName)); } context.Services.AddRabbitMqConnection(); context.Services.Configure<AegisRebusOptions>(options => { options.ConfigureLibraryDefault(); options.UseRabbitMq(); }); context.Services.Configure<AegisRebusRabbitMqOptions>(options => { options.QueueName = queueName; }); }
在使用的时候只需要传入对于的队列名称即可,UseRabbitMq()会根据传入的队列名称进行声明绑定:
public static void UseRabbitMq(this AegisRebusOptions rebusOptions, string connectionString = null, string inputQueueName = null, Action<RabbitMqOptionsBuilder> optionsAction = null) { rebusOptions.Configure(context => { var rabbitMqOptions = context.ServiceProvider .GetRequiredService<IOptions<AegisRebusRabbitMqOptions>>().Value; connectionString ??= rabbitMqOptions.RabbitMqConnection; inputQueueName ??= rabbitMqOptions.QueueName; context.Transport(t => { var builder = t.UseRabbitMq(connectionString, inputQueueName); optionsAction?.Invoke(builder); }); }); }
OK,队列有了之后,那如何将队列与Routing key来进行绑定呢,我们在实际使用中一个Routing key就代表了一个消费方法或者接口。为了统一管理Routing key的声明和绑定,我们设定了一个统一的泛型接口:IDistributedEventHandler
public interface IDistributedEventHandler<in TEvent> : IEventHandler { /// <summary> /// Handler handles the event by implementing this method. /// </summary> /// <param name="eventData">Event data</param> Task HandleEventAsync(TEvent eventData); }
那这个泛型接口有什么用的,首先所有的消费类都需要继承于它,类似于这样:
public class TestWebMQHandler : IDistributedEventHandler<TestMQEvent>, ITransientDependency { public async Task HandleEventAsync(TestMQEvent eventData) { string result = eventData.SendMessage; if (result != null) { result = "test"; } await Task.FromResult(result); } }
namespace TestWeb.Domain { public class TestMQEvent { public string SendMessage { get; set; } } }
TestMQEvent这个是我自定义的消息内容类,你可以理解为消息的入参,就是我们要消费的消息。继承IDistributedEventHandler接口之后,程序默认会根据消息内容类的程序集生成Routing Key,方式就是NameSpace+类名。
那如上这个实例消费类会生成的Routing Key是:TestWeb.Domain.TestMQEvent, TestWeb.Domain,实际效果如下:
OK,Routing Key生成之后又是如何与队列绑定的呢,也就是消费者如何订阅的。
项目运行时,消费者的订阅时统一处理的,我们上面说·所有的消费类都需要继承 IDistributedEventHandler 这个泛型接口。
我们首先会通过反射拿到所有继承于IDistributedEventHandler的消息类,类似上面就是TestWebMQHandler,然后获取对应的消息提类来绑定到队列上:
private static void AddEventHandlers(IServiceCollection services) { var localHandlers = new List<Type>(); var distributedHandlers = new List<Type>(); services.OnRegistered(context => { if (ReflectionHelper.IsAssignableToGenericType(context.ImplementationType, typeof(ILocalEventHandler<>))) { localHandlers.Add(context.ImplementationType); } else if (ReflectionHelper.IsAssignableToGenericType(context.ImplementationType, typeof(IDistributedEventHandler<>))) { distributedHandlers.Add(context.ImplementationType); } }); services.Configure<LocalEventBusOptions>(options => { options.Handlers.AddIfNotContains(localHandlers); }); services.Configure<DistributedEventBusOptions>(options => { options.Handlers.AddIfNotContains(distributedHandlers); }); }
上面时拿到对应的消费方法类,接着获取消息内容类,从而调用订阅方法:
private void Initialize(IEnumerable<Type> handleTypes) { foreach (var handleType in handleTypes) { var interfaces = handleType.GetInterfaces(); foreach (var @interface in interfaces) { if (!typeof(IEventHandler).IsAssignableFrom(@interface)) { continue; } var genericArgs = @interface.GetGenericArguments(); if (genericArgs.Length != 1) continue; var types = _handlerTypes.GetOrAdd(genericArgs[0], new List<Type>()); types.AddIfNotContains(handleType); } } }
public static IServiceProvider UseRebus(this IServiceProvider provider) { var bus = provider.GetRequiredService<IBus>(); var activator = provider.GetRequiredService<IAegisDistributedEventHandlerActivator>(); foreach (var eventType in activator.GetAllEventTypes()) { bus.Subscribe(eventType); } return provider; }
核心的逻辑就是这样,在使用中调用Rebus提供的发布方法即可。
这样的使用方式将消费处理剥离出来,而不是将生产和消费的处理程序集成在同一个服务器上,方便快速增加消费者横向扩容。