[C#] Furion Jaina + RabbitMQ 示例
概述
Furion Jaina + RabbitMQ (https://furion.baiqian.ltd/docs/event-bus)示例
逻辑
1.事件源存储器(RabbitMQEventSourceStorer)订阅消息(监听RabbitMQ Queue)。
2.当RabbitMQ Queue收到消息后,事件源存储器(RabbitMQEventSourceStorer)会将其转化为指定的事件源类型(RabbitMQEventSource)并存入Channel,如果转化失败(事件源类型不匹配),消息会回到RabbitMQ,进入Unacked状态。
Unacked : 表明消息没有消费成功,当RabbitMQ Queue不再有对象进行订阅和消费时,消息会重新回到Ready状态。这种消息一般都是数据或格式存在问题,可通过重启RabbitMQ, 删除Queue,停止Queue订阅等形式删除消息。
稳健删除的方式是停止Queue订阅,让消息回到Ready状态,此时可通过RabbitMQ Management点击Queue,使用Purge Messages删除Queue中所有消息。另外可在删除前通过Get Message(s)检查消息内容。
3.事件源进入到Channel后,事件源订阅者(ToDoEventSubscriber)将根据事件源的EventId执行相应的方法。
如果有事件执行监视器(ToDoEvenetHandlerMonitor),事件执行器(RetryEventHandlerExecutor),则执行顺序为:
- ToDoEvenetHandlerMonitor.OnExecutingAsysn()
- RetryEventHandlerExecutor.ExecuteAsync()
- ToDoEventSubscriber.Method()
- ToDoEvenetHandlerMonitor.OnExecuedAsysn()
事件源
定义RabbitMQ消息格式。这里EventId是必须的,事件订阅者将通过特性([EventSubscribe("EventId值")])执行相应的方法。
public class RabbitMQEventSource : IEventSource { public RabbitMQEventSource() { } public RabbitMQEventSource(string eventId, object payload) { EventId = eventId; Payload = payload; } /// <summary> /// 事件 Id /// </summary> public string EventId { get; set; } /// <summary> /// 事件承载(携带)数据 /// </summary> public object Payload { get; set; } /// <summary> /// 事件创建时间 /// </summary> public DateTime CreatedTime { get; set; } = DateTime.UtcNow; /// <summary> /// 取消任务 Token /// </summary> /// <remarks>用于取消本次消息处理</remarks> [Newtonsoft.Json.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore] public CancellationToken CancellationToken { get; set; } }
事件源存储器
这里使用了System.Threading.Channels的Channel作为事件源存储器。
通过RabbitMQ订阅消息并写入到事件源存储器中。
符合事件源的数据将写入到事件源存储器并应答,之后开始消费;
不符合事件源的数据将变为Unacked状态,可在RabbitMQ管理界面查看队列消息状态。
public sealed class RabbitMQEventSourceStorer : IEventSourceStorer, IDisposable { /// <summary> /// 内存通道事件源存储器 /// </summary> private readonly Channel<IEventSource> _channel; /// <summary> /// 通道对象 /// </summary> private readonly IModel _model; /// <summary> /// 连接对象 /// </summary> private readonly IConnection _connection; /// <summary> /// 路由键 /// </summary> private readonly string _routeKey; /// <summary> /// 构造函数 /// </summary> /// <param name="factory">连接工厂</param> /// <param name="routeKey">路由键</param> /// <param name="capacity">存储器最多能够处理多少消息,超过该容量进入等待写入</param> public RabbitMQEventSourceStorer(ConnectionFactory factory, string routeKey, int capacity) { // 配置通道,设置超出默认容量后进入等待 var boundedChannelOptions = new BoundedChannelOptions(capacity) { FullMode = BoundedChannelFullMode.Wait }; // 创建有限容量通道 _channel = Channel.CreateBounded<IEventSource>(boundedChannelOptions); // 创建连接 _connection = factory.CreateConnection(); _routeKey = routeKey; // 创建通道 _model = _connection.CreateModel(); // 声明路由队列 _model.QueueDeclare(routeKey, false, false, false, null); // 创建消息订阅者 var consumer = new EventingBasicConsumer(_model); // 订阅消息并写入内存 Channel consumer.Received += (ch, ea) => { // 读取原始消息 var stringEventSource = Encoding.UTF8.GetString(ea.Body.ToArray()); // 转换为 IEventSource,这里可以选择自己喜欢的序列化工具,如果自定义了 EventSource,注意属性是可读可写 var eventSource = JsonSerializer.Deserialize<RabbitMQEventSource>(stringEventSource); // 写入内存管道存储器 _channel.Writer.WriteAsync(eventSource); // 确认该消息已被消费 _model.BasicAck(ea.DeliveryTag, false); }; // 启动消费者 设置为手动应答消息 _model.BasicConsume(routeKey, false, consumer); } /// <summary> /// 将事件源写入存储器 /// </summary> /// <param name="eventSource">事件源对象</param> /// <param name="cancellationToken">取消任务 Token</param> /// <returns><see cref="ValueTask"/></returns> public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) { // 空检查 if (eventSource == default) { throw new ArgumentNullException(nameof(eventSource)); } // 这里判断是否是 ChannelEventSource 或者 自定义的 EventSource if (eventSource is RabbitMQEventSource source) { // 序列化,这里可以选择自己喜欢的序列化工具 var data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(source)); // 发布 _model.BasicPublish("", _routeKey, null, data); } else { // 这里处理动态订阅问题 await _channel.Writer.WriteAsync(eventSource, cancellationToken); } } /// <summary> /// 从存储器中读取一条事件源 /// </summary> /// <param name="cancellationToken">取消任务 Token</param> /// <returns>事件源对象</returns> public async ValueTask<IEventSource> ReadAsync(CancellationToken cancellationToken) { // 读取一条事件源 var eventSource = await _channel.Reader.ReadAsync(cancellationToken); return eventSource; } /// <summary> /// 释放非托管资源 /// </summary> public void Dispose() { _model.Dispose(); _connection.Dispose(); } }
事件订阅者
事件源存储器在写入事件源之后,紧接着便是自动执行ReadAsync方法。
这时事件订阅者将通过特性([EventSubscribe("EventId值")])执行相应的方法。
如果配置了事件执行监视器,会在执行方法前后调用。
public class ToDoEventSubscriber : IEventSubscriber { private readonly ILogger<ToDoEventSubscriber> _logger; public ToDoEventSubscriber(ILogger<ToDoEventSubscriber> logger) { _logger = logger; } [EventSubscribe("ToDo:Create")] public async Task CreateToDo(EventHandlerExecutingContext context) { var todo = context.Source; _logger.LogInformation("创建一个 ToDo:{Name}", todo.Payload); await Task.CompletedTask; } }
事件执行监视器
事件订阅者执行方法前后会调用。
public class ToDoEventHandlerMonitor : IEventHandlerMonitor { private readonly ILogger<ToDoEventHandlerMonitor> _logger; public ToDoEventHandlerMonitor(ILogger<ToDoEventHandlerMonitor> logger) { _logger = logger; } public Task OnExecutingAsync(EventHandlerExecutingContext context) { _logger.LogInformation("执行之前:{EventId}", context.Source.EventId); return Task.CompletedTask; } public Task OnExecutedAsync(EventHandlerExecutedContext context) { _logger.LogInformation("执行之后:{EventId}", context.Source.EventId); if (context.Exception != null) { _logger.LogError(context.Exception, "执行出错啦:{EventId}", context.Source.EventId); } return Task.CompletedTask; } }
事件执行器
如果配置了事件执行器,会在事件订阅者执行方法前执行。
public class RetryEventHandlerExecutor : IEventHandlerExecutor { public async Task ExecuteAsync(EventHandlerExecutingContext context, Func<EventHandlerExecutingContext, Task> handler) { // 如果执行失败,每隔 1s 重试,最多三次 await Retry.InvokeAsync(async () => { await handler(context); }, 3, 1000); } }
Startup
ConnectionFactory配置RabbitMQ的HostName,UserName和Password.
New RabbitMQEventSourceStorer这里第二个参数传入RabbitMQ队列的Routing key.
public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // 注册 EventBus 服务 services.AddEventBus(options => { // 注册 ToDo 事件订阅者 options.AddSubscriber<ToDoEventSubscriber>(); // 创建连接工厂 var factory = new ConnectionFactory { HostName = "XX.XX.XX.XX", UserName = "XXXXXX", Password = "XXXXXX", }; // 创建默认内存通道事件源对象,可自定义队列路由key,比如这里是 eventbus var rbmqEventSourceStorer = new RabbitMQEventSourceStorer(factory, "XXXXX", 3000); // 替换默认事件总线存储器 options.ReplaceStorer(serviceProvider => { return rbmqEventSourceStorer; }); // 添加事件执行监视器 options.AddMonitor<ToDoEventHandlerMonitor>(); // 添加事件执行器 options.AddExecutor<RetryEventHandlerExecutor>(); }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
ApiController
WriteMessage通过eventSourceStorer(事件源存储器)发布事件源。
PublishMessage通过eventPublisher发布事件源。
两个方法最终都是调用事件源存储器中的WriteAsync方法发布事件源。
具体逻辑是将符合事件源类型的数据发布到RabbitMQ,之后RabbitMQ的订阅事件(在事件源存储器的构造函数里)将会监听到这些发布的事件源。
[Route("api/[controller]")] [ApiController] public class ToDoController : ControllerBase { // 依赖注入事件发布者 IEventPublisher private readonly IEventPublisher _eventPublisher; private readonly IEventSourceStorer _eventSourceStorer; public ToDoController(IEventPublisher eventPublisher, IEventSourceStorer eventSourceStorer) { _eventPublisher = eventPublisher; _eventSourceStorer = eventSourceStorer; } [HttpGet("WriteMessage")] public async void WriteMessage(CancellationToken cancellationToken) { RabbitMQEventSource source = new RabbitMQEventSource("ToDo:Create", "WriteMessage"); await _eventSourceStorer.WriteAsync(source, cancellationToken); Console.WriteLine(); } [HttpGet("PublishMessage")] public async void PublishMessage() { RabbitMQEventSource source = new RabbitMQEventSource("ToDo:Create", "PublishMessage"); await _eventPublisher.PublishAsync(source); Console.WriteLine(); } }
RabbitMQ配置
1.创建一个Queue, Durability 要与 事件源存储器(RabbitMQEventSourceStorer)中的队列持久化参数一致,示例(_model.QueueDeclare(routeKey , false, false, false, null); )的第二个参数为false , 所以此处Durability选择Transient.
2. 在Exchanges ,点击一个Exchange,并Bind一个Queue. 此处需要填写Queue Name 和 Routing Key (自定义值) , 之后点击Bind.
Routing Key 需要配置到 Startup中(var rbmqEventSourceStorer = new RabbitMQEventSourceStorer(factory,"XXXXX",3000);),替换第二个参数为Routing Key.