原始碼解析-Abp vNext丨分散式事件匯流排DistributedEventBus

初久的私房菜發表於2021-10-31

前言

上一節我們們講了LocalEventBus,本節來講本地事件匯流排(DistributedEventBus),採用的RabbitMQ進行實現。

Volo.Abp.EventBus.RabbitMQ模組內部程式碼並不多,RabbitMQ的操作都集中在Volo.Abp.RabbitMQ這個包中。

正文

我們從模組定義開始看,專案啟動的時候分別讀取了appsetting.json的配置引數和呼叫了RabbitMqDistributedEventBusInitialize函式。

    public class AbpEventBusRabbitMqModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var configuration = context.Services.GetConfiguration();

            Configure<AbpRabbitMqEventBusOptions>(configuration.GetSection("RabbitMQ:EventBus"));
        }

        public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            context
                .ServiceProvider
                .GetRequiredService<RabbitMqDistributedEventBus>()
                .Initialize();
        }
    }

Initialize函式中我們根據 MessageConsumerFactory.Create向內部進行查閱可以看到最終呼叫方法為RabbitMqMessageConsumer.TryCreateChannelAsync並且在其內部我們可以看到下面程式碼,這裡定義了消費的回撥函式。反推Initialize方法其實是在啟動一個消費者。

      public void Initialize()
        {
            Consumer = MessageConsumerFactory.Create(
                new ExchangeDeclareConfiguration(
                    AbpRabbitMqEventBusOptions.ExchangeName,
                    type: "direct",
                    durable: true
                ),
                new QueueDeclareConfiguration(
                    AbpRabbitMqEventBusOptions.ClientName,
                    durable: true,
                    exclusive: false,
                    autoDelete: false
                ),
                AbpRabbitMqEventBusOptions.ConnectionName
            );

            Consumer.OnMessageReceived(ProcessEventAsync);

            SubscribeHandlers(AbpDistributedEventBusOptions.Handlers);
        }

 var consumer = new AsyncEventingBasicConsumer(Channel);
                consumer.Received += HandleIncomingMessageAsync;

繼續向下看Consumer.OnMessageReceived(ProcessEventAsync);該方法向一個併發安全集合輸入一個委託事件,並該事件會在上面的HandleIncomingMessageAsync會調中觸發故確定為消費者的執行邏輯,而ProcessEventAsync其實還是走了我們在講LocalEventBus哪一套,尋找Handler執行函式。

SubscribeHandlers還是上節講的基類的函式,這裡要注意內部呼叫的Subscribe該方法中的 Consumer.BindAsync會根據為消費者Bind路由,這樣才能觸發事件處理函式。


       public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory)
        {
            var handlerFactories = GetOrCreateHandlerFactories(eventType);

            if (factory.IsInFactories(handlerFactories))
            {
                return NullDisposable.Instance;
            }

            handlerFactories.Add(factory);

            if (handlerFactories.Count == 1) //TODO: Multi-threading!
            {
                Consumer.BindAsync(EventNameAttribute.GetNameOrDefault(eventType));
            }

            return new EventHandlerFactoryUnregistrar(this, eventType, factory);
        }

看完了事件消費者我們來看看事件釋出,直接看PublishAsync函式就完事了,整個函式非常簡單,都是RabbitMQ的操作語法,這裡的路由Key是在EventNameAttribute.GetNameOrDefault(eventType);函式中通過讀取ETO上指定註解Name來指定的。

protected Task PublishAsync(
            string eventName,
            byte[] body,
            IBasicProperties properties,
            Dictionary<string, object> headersArguments = null,
            Guid? eventId = null)
        {
            using (var channel = ConnectionPool.Get(AbpRabbitMqEventBusOptions.ConnectionName).CreateModel())
            {
                channel.ExchangeDeclare(
                    AbpRabbitMqEventBusOptions.ExchangeName,
                    "direct",
                    durable: true
                );

                if (properties == null)
                {
                    properties = channel.CreateBasicProperties();
                    properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent;
                }

                if (properties.MessageId.IsNullOrEmpty())
                {
                    properties.MessageId = (eventId ?? GuidGenerator.Create()).ToString("N");
                }

                SetEventMessageHeaders(properties, headersArguments);

                channel.BasicPublish(
                    exchange: AbpRabbitMqEventBusOptions.ExchangeName,
                    routingKey: eventName,
                    mandatory: true,
                    basicProperties: properties,
                    body: body
                );
            }

            return Task.CompletedTask;
        }

解析

整個分散式事件的實現其實非常簡單,在事件發生時釋出者只需要定義好路由名稱和訊息內容傳送RabbitMQ中,而消費者則是在專案執行的時候的通過呼叫Initialize就啟動起來了。

這裡我們也同樣根據整個原理自己實現一下這個流程。

Dppt.EventBus分別定義IDistributedEventBus、DistributedEventBusOptions、IDistributedEventHandler分別用於採用分散式事件匯流排呼叫、配置選項用於儲存處理程式Handler、定義分散式處理程式抽象。

新建Dppt.EventBus.RabbitMQ類庫先簡單對RabbitMQ進行一個簡單的封裝

public class RabbitMqConnections : IRabbitMqConnections
    {
        private readonly IConnectionFactory _connectionFactory;
        private readonly ILogger<RabbitMqConnections> _logger;
        IConnection _connection;
        bool _disposed;
        public RabbitMqConnections(IConnectionFactory connectionFactory, ILogger<RabbitMqConnections> logger)
        {
            _connectionFactory = connectionFactory;
            _logger = logger;
        }


        public bool IsConnected
        {
            get
            {
                return _connection != null && _connection.IsOpen && !_disposed;
            }
        }

        public void TryConnect() {

            _connection = _connectionFactory.CreateConnection();

        }


        public IModel CreateModel()
        {
            if (!IsConnected)
            {
                throw new InvalidOperationException("No RabbitMQ connections are available to perform this action");
            }

            return _connection.CreateModel();
        }


        public void Dispose()
        {
            if (_disposed) return;

            _disposed = true;

            try
            {
                _connection.Dispose();
            }
            catch (IOException ex)
            {
                _logger.LogCritical(ex.ToString());
            }
        }

    }

然後我們分別定義ExchangeDeclareConfiguration、QueueDeclareConfiguration用於記錄配置資訊。

開始處理RabbitMqEventBus處理程式首先是釋出事件,大體程式碼如下就是往RabbitMQ裡面丟訊息。

        /// <summary>
        /// rabbmitmq 連線服務
        /// </summary>
        public readonly IRabbitMqConnections _rabbitMqConnections;


public Task PublishAsync<TEvent>(TEvent eventData)
        {
            var eventName = EventNameAttribute.GetNameOrDefault(typeof(TEvent));
            var body = JsonSerializer.Serialize(eventData);
            return PublishAsync(eventName, body, null, null);
        }

        public Task PublishAsync(string eventName, string body, IBasicProperties properties, Dictionary<string, object> headersArguments = null, Guid? eventId = null)
        {

            if (!_rabbitMqConnections.IsConnected)
            {
                _rabbitMqConnections.TryConnect();
            }
            using (var channel = _rabbitMqConnections.CreateModel())
            {
                // durable 設定佇列持久化  
                channel.ExchangeDeclare(RabbitMqEventBusOptions.ExchangeName, "direct", durable: true);

                if (properties == null)
                {
                    properties = channel.CreateBasicProperties();
                    // 設定訊息持久化
                    properties.DeliveryMode = 2;
                }

                if (properties.MessageId.IsNullOrEmpty())
                {
                    // 訊息的唯一性標識
                    properties.MessageId = (eventId ?? Guid.NewGuid()).ToString("N");
                }

                SetEventMessageHeaders(properties, headersArguments);

                channel.BasicPublish(
                   exchange: RabbitMqEventBusOptions.ExchangeName,
                   routingKey: eventName,
                   mandatory: true,
                   basicProperties: properties,
                   body: Encoding.UTF8.GetBytes(body)
               );

            }

            return Task.CompletedTask;
        }

      private void SetEventMessageHeaders(IBasicProperties properties, Dictionary<string, object> headersArguments)
        {
            if (headersArguments == null)
            {
                return;
            }

            properties.Headers ??= new Dictionary<string, object>();

            foreach (var header in headersArguments)
            {
                properties.Headers[header.Key] = header.Value;
            }
        }


然後就是消費者的處理,我們同樣定義Initialize函式,並簡化部分封裝程式碼,完成消費者啟動。

 public void Initialize()
        {

            Exchange = new ExchangeDeclareConfiguration(RabbitMqEventBusOptions.ExchangeName,"direct",true);
            Queue = new QueueDeclareConfiguration(RabbitMqEventBusOptions.ClientName, true, false, false);

            // 啟動一個消費者
            if (!_rabbitMqConnections.IsConnected)
            {
                _rabbitMqConnections.TryConnect();
            }

            try
            {

                Channel = _rabbitMqConnections.CreateModel();



                Channel.ExchangeDeclare(
                  exchange: Exchange.ExchangeName,
                  type: Exchange.Type,
                  durable: Exchange.Durable,
                  autoDelete: Exchange.AutoDelete,
                  arguments: Exchange.Arguments
              );


                Channel.QueueDeclare(
                   queue: Queue.QueueName,
                   durable: Queue.Durable,
                   exclusive: Queue.Exclusive,
                   autoDelete: Queue.AutoDelete,
                   arguments: Queue.Arguments
               );

                var consumer = new AsyncEventingBasicConsumer(Channel);
                consumer.Received += HandleIncomingMessageAsync;

                Channel.BasicConsume(
                    queue: Queue.QueueName,
                    autoAck: false,
                    consumer: consumer
                );

                SubscribeHandlers(DistributedEventBusOptions.Handlers);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
        }

引數配置這邊主要是讀取AppSetting資訊和索要Handler

 public static class DpptEventBusRabbitMqRegistrar
    {
        public static void AddDpptEventBusRabbitMq(this IServiceCollection services, IConfiguration configuration, List<Type> types)
        {
     
            services.AddSingleton<IRabbitMqConnections>(sp =>
            {
                var logger = sp.GetRequiredService<ILogger<RabbitMqConnections>>();

                var factory = new ConnectionFactory()
                {
                    HostName = configuration["RabbitMQ:EventBusConnection"],
                    VirtualHost = configuration["RabbitMQ:EventBusVirtualHost"],
                    DispatchConsumersAsync = true,
                    AutomaticRecoveryEnabled = true
            };

                if (!string.IsNullOrEmpty(configuration["RabbitMQ:EventBusUserName"]))
                {
                    factory.UserName = configuration["RabbitMQ:EventBusUserName"];
                }

                if (!string.IsNullOrEmpty(configuration["RabbitMQ:EventBusPassword"]))
                {
                    factory.Password = configuration["RabbitMQ:EventBusPassword"];
                }

                return new RabbitMqConnections(factory, logger);
            });

            var distributedHandlers = types;
            foreach (var item in distributedHandlers)
            {
                services.AddSingleton(item);
            }

            services.Configure<DistributedEventBusOptions>(options =>
            {
                options.Handlers.AddIfNotContains(distributedHandlers);
            });

            services.Configure<DpptRabbitMqEventBusOptions>(options => {

                options.ExchangeName = configuration["RabbitMQ:EventBus:ExchangeName"];
                options.ClientName = configuration["RabbitMQ:EventBus:ClientName"];
            });

            services.AddSingleton<IDistributedEventBus, RabbitMqDistributedEventBus>();

          
        }
    }

測試

新建一個空專案,進行外掛註冊,然後建立ETO和Handler進行測試。

64

測試結果放在下面了。

62

63

結語

本次挑選了一個比較簡單的示例來講,整個EventBus我應該分成3篇 下一篇我來講分散式事務。

最後歡迎各位讀者關注我的部落格, https://github.com/MrChuJiu/Dppt/tree/master/src 歡迎大家Star

另外這裡有個社群地址(https://github.com/MrChuJiu/Dppt/discussions),如果大家有技術點希望我提前檔期可以寫在這裡,希望本專案助力我們一起成長

相關文章