[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),则执行顺序为:

  1. ToDoEvenetHandlerMonitor.OnExecutingAsysn() 
  2. RetryEventHandlerExecutor.ExecuteAsync() 
  3. ToDoEventSubscriber.Method() 
  4. 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.

 

posted @ 2023-01-12 17:02  WikiChen  阅读(971)  评论(0编辑  收藏  举报