1. 引言

事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉。事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。



从上图可知,核心就4个角色:

  1. 事件(事件源+事件处理)
  2. 事件发布者
  3. 事件订阅者
  4. 事件总线

实现事件总线的关键是:

  1. 事件总线维护一个事件源与事件处理的映射字典;
  2. 通过单例模式,确保事件总线的唯一入口;
  3. 利用反射完成事件源与事件处理的初始化绑定;
  4. 提供统一的事件注册、取消注册和触发接口。

以上源于我在事件总线知多少(1)中对于EventBus的分析和简单总结。基于以上的简单认知,我们来梳理下eShopOnContainers中EventBus的实现机制·。

2. 高屋建瓴--看类图

我们直接以上帝视角,来看下其实现机制,上类图。

我们知道事件的本质是:事件源+事件处理

针对事件源,其定义了IntegrationEvent基类来处理。默认仅包含一个guid和一个创建日期,具体的事件可以通过继承该类,来完善事件的描述信息。

这里有必要解释下Integration Event(集成事件)。因为在微服务中事件的消费不再局限于当前领域内,而是多个微服务可能共享同一个事件,所以这里要和DDD中的领域事件区分开来。集成事件可用于跨多个微服务或外部系统同步领域状态,这是通过在微服务之外发布集成事件来实现的。

针对事件处理,其本质是对事件的反应,一个事件可引起多个反应,所以,它们之间是一对多的关系。

eShopOnContainers中抽象了两个事件处理的接口:

  1. IIntegrationEventHandler
  2. IDynamicIntegrationEventHandler

二者都定义了一个Handle方法用于响应事件。不同之处在于方法参数的类型:

第一个接受的是一个强类型的IntegrationEvent。第二个接收的是一个动态类型dynamic

为什么要单独提供一个事件源为dynamic类型的接口呢?

不是每一个事件源都需要详细的事件信息,所以一个强类型的参数约束就没有必要,通过dynamic可以简化事件源的构建,更趋于灵活。

有了事件源和事件处理,接下来就是事件的注册和订阅了。为了方便进行订阅管理,系统提供了额外的一层抽象IEventBusSubscriptionsManager,其用于维护事件的订阅和注销,以及订阅信息的持久化。其默认的实现InMemoryEventBusSubscriptionsManager就是使用内存进行存储事件源和事件处理的映射字典。

从类图中看InMemoryEventBusSubscriptionsManager中定义了一个内部类SubscriptionInfo,其主要用于表示事件订阅方的订阅类型和事件处理的类型。

我们来近距离看下InMemoryEventBusSubscriptionsManager的定义:

//InMemoryEventBusSubscriptionsManager.cs
//定义的事件名称和事件订阅的字典映射(1:N)
private readonly Dictionary<string, List<SubscriptionInfo>> _handlers;
//保存所有的事件处理类型
private readonly List<Type> _eventTypes;
//定义事件移除后事件
public event EventHandler<string> OnEventRemoved; //构造函数初始化
public InMemoryEventBusSubscriptionsManager()
{
_handlers = new Dictionary<string, List<SubscriptionInfo>>();
_eventTypes = new List<Type>();
}
//添加动态类型事件订阅(需要手动指定事件名称)
public void AddDynamicSubscription<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler
{
DoAddSubscription(typeof(TH), eventName, isDynamic: true);
}
//添加强类型事件订阅(事件名称为事件源类型)
public void AddSubscription<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
var eventName = GetEventKey<T>(); DoAddSubscription(typeof(TH), eventName, isDynamic: false); if (!_eventTypes.Contains(typeof(T)))
{
_eventTypes.Add(typeof(T));
}
}
//移除动态类型事件订阅
public void RemoveDynamicSubscription<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler
{
var handlerToRemove = FindDynamicSubscriptionToRemove<TH>(eventName);
DoRemoveHandler(eventName, handlerToRemove);
} //移除强类型事件订阅
public void RemoveSubscription<T, TH>()
where TH : IIntegrationEventHandler<T>
where T : IntegrationEvent
{
var handlerToRemove = FindSubscriptionToRemove<T, TH>();
var eventName = GetEventKey<T>();
DoRemoveHandler(eventName, handlerToRemove);
}

添加了这么一层抽象,即符合了单一职责原则,又完成了代码重用。IEventBus的具体实现通过注入对IEventBusSubscriptionsManager的依赖,即可完成订阅管理。

你这里可能会好奇,为什么要暴露一个OnEventRemoved事件?这里先按住不表,留给大家思考。

3. 使用RabbitMQ实现EventBus

3.1. 为什么需要RabbitMQ?

微服务的一大特点就是分布式。若需要做到动一发而牵全身,就需要一个持久化的集中式的EventBus。这就要求各个微服务内部虽然分别持有一个对EventBus的引用,但它们背后都必须连接着同一个用于持久化的数据源。

那你可能会说:那这个很好实现,使用同一个数据库就好了。为什么非要用个什么RabbitMQ?问的好!这就要去探讨下RabbitMQ是为了解决什么问题了。

RabbitMQ提供了可靠的消息机制、跟踪机制和灵活的消息路由,支持消息集群和分布式部署。适用于排队算法、秒杀活动、消息分发、异步处理、数据同步、处理耗时任务、CQRS等应用场景。

而关于RabbitMQ的具体使用,这里不再展开,可参考RabbitMQ知多少

3.2. EventBus集成RabbitMQ的核心

集成RabbitMQ的关键在于理解其对消息的处理机制:

  1. 消息的生产者和消费者通过与服务器(Broker)建立连接,然后基于创建的信道(Chanel)进行消息的发生和接收。
  2. 消息的生产者可以通过声明指定的队列(queue)或交换机(exchange)以及路由(routingKey)进行消息的发送。
  3. 消息的消费者通过绑定到相应的队列(queue)或交换机(exchange)监听相应的路由(routingKey),进行消息的接收。
  4. 消息的消费者通过构造消费者实例绑定消息接收后的事件委托来进行消息消费。

3.3. 源码一览

基于以上的认知,我们再与EventBusRabbitMQ源码亲密接触。

3.3.1. 构造函数定义

public class EventBusRabbitMQ : IEventBus, IDisposable
{
const string BROKER_NAME = "eshop_event_bus"; private readonly IRabbitMQPersistentConnection _persistentConnection;
private readonly ILogger<EventBusRabbitMQ> _logger;
private readonly IEventBusSubscriptionsManager _subsManager;
private readonly ILifetimeScope _autofac;
private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
private readonly int _retryCount; private IModel _consumerChannel;
private string _queueName; public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger<EventBusRabbitMQ> logger,
ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5)
{
_persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager();
_queueName = queueName;
_consumerChannel = CreateConsumerChannel();
_autofac = autofac;
_retryCount = retryCount;
_subsManager.OnEventRemoved += SubsManager_OnEventRemoved;
} private void SubsManager_OnEventRemoved(object sender, string eventName)
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
} using (var channel = _persistentConnection.CreateModel())
{
channel.QueueUnbind(queue: _queueName, exchange: BROKER_NAME, routingKey: eventName); if (_subsManager.IsEmpty)
{
_queueName = string.Empty;
_consumerChannel.Close();
}
}
}
//....
}

构造函数主要做了以下几件事:

  1. 注入IRabbitMQPersistentConnection以便连接到对应的Broke。
  2. 使用空对象模式注入IEventBusSubscriptionsManager ,进行订阅管理。
  3. 创建消费者信道,用于消息消费。
  4. 注册OnEventRemoved事件,取消队列的绑定。(这也就回答了上面遗留的问题)

3.3.2. 事件订阅的逻辑:

private void DoInternalSubscription(string eventName)
{
var containsKey = _subsManager.HasSubscriptionsForEvent(eventName);
if (!containsKey)
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
} using (var channel = _persistentConnection.CreateModel())
{
channel.QueueBind(queue: _queueName,
exchange: BROKER_NAME,
routingKey: eventName);
}
}
}

从上面我们可以看到事件的订阅主要是进行rabbitmq队列的绑定。以eventName为routingKey进行路由。

3.3.3. 事件的发布逻辑

public void Publish(IntegrationEvent @event)
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
} var policy = RetryPolicy.Handle<BrokerUnreachableException>()
.Or<SocketException>()
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>
{
_logger.LogWarning(ex.ToString());
}); using (var channel = _persistentConnection.CreateModel())
{
var eventName = @event.GetType()
.Name; channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); var message = JsonConvert.SerializeObject(@event);
var body = Encoding.UTF8.GetBytes(message); policy.Execute(() =>
{
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2; // persistent channel.BasicPublish(exchange: BROKER_NAME, routingKey: eventName, mandatory:true, basicProperties: properties, body: body);
});
}
}

这里面有以下几个知识点:

  1. 使用Polly,以2的阶乘的时间间隔进行重试。(第一次2s后,第二次4s后,第三次8s后...重试)
  2. 使用direct全匹配、单播形式的路由机制进行消息分发
  3. 消息主体是格式化的json字符串
  4. 指定DeliveryMode = 2进行消息持久化
  5. 指定mandatory: true告知服务器当根据指定的routingKey和消息找不到对应的队列时,直接返回消息给生产者。

3.3.4. 然后看看事件消息的监听

private IModel CreateConsumerChannel()
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
}
var channel = _persistentConnection.CreateModel();
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
channel.QueueDeclare(queue: _queueName, durable: true, exclusive: false,autoDelete: false, arguments: null);
var consumer = new EventingBasicConsumer(channel);
consumer.Received += async (model, ea) =>
{
var eventName = ea.RoutingKey;
var message = Encoding.UTF8.GetString(ea.Body);
await ProcessEvent(eventName, message);
channel.BasicAck(ea.DeliveryTag, multiple:false);
};
channel.BasicConsume(queue: _queueName, autoAck: false, consumer: consumer);
channel.CallbackException += (sender, ea) =>
{
_consumerChannel.Dispose();
_consumerChannel = CreateConsumerChannel();
};
return channel;
}

以上代码演示了如创建消费信道进行消息处理的步骤:

  1. 创建信道Channel
  2. 并申明Exchange
  3. 实例化绑定Channel的消费者实例
  4. 注册Received事件委托处理消息接收事件
  5. 调用channel.BasicConsume启动监听

3.3.5. 具体的事件处理

private async Task ProcessEvent(string eventName, string message)
{
if (_subsManager.HasSubscriptionsForEvent(eventName))
{
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
{
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
{
if (subscription.IsDynamic)
{
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
dynamic eventData = JObject.Parse(message);
await handler.Handle(eventData);
}
else
{
var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
var handler = scope.ResolveOptional(subscription.HandlerType);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
}
}
}
}
}

以上代码主要包括以下知识点:

  1. Json字符串的反序列化
  2. 利用依赖注入容器解析集成事件(Integration Event)和事件处理(Event Handler)类型
  3. 反射调用具体的事件处理方法

4. EventBus的集成和使用

以上介绍了EventBus的实现要点,那各个微服务是如何集成呢?

1. 注册IRabbitMQPersistentConnection服务用于设置RabbitMQ连接

services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
//...
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
});

2. 注册单例模式的IEventBusSubscriptionsManager用于订阅管理

services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();

3. 注册单例模式的EventBusRabbitMQ

services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
{
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); var retryCount = 5;
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
} return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount);
});

完成了以上集成,就可以在代码中使用事件总线,进行事件的发布和订阅。

4. 发布事件

若要发布事件,需要根据是否需要事件源(参数传递)来决定是否需要申明相应的集成事件,需要则继承自IntegrationEvent进行申明。然后在需要发布事件的地方进行实例化,并通过调用IEventBus的实例的Publish方法进行发布。

//事件源的声明
public class ProductPriceChangedIntegrationEvent : IntegrationEvent
{
public int ProductId { get; private set; } public decimal NewPrice { get; private set; } public decimal OldPrice { get; private set; } public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice)
{
ProductId = productId;
NewPrice = newPrice;
OldPrice = oldPrice;
}
}
//声明事件源
var priceChangedEvent = new ProductPriceChangedIntegrationEvent(1001, 200.00, 169.00)
//发布事件
_eventBus.Publish(priceChangedEvent)

5. 订阅事件

若要订阅事件,需要根据需要处理的事件类型,申明对应的事件处理类,继承自IIntegrationEventHandlerIDynamicIntegrationEventHandler,并注册到IOC容器。然后创建IEventBus的实例调用Subscribe方法进行显式订阅。

//定义事件处理
public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
{
public async Task Handle(ProductPriceChangedIntegrationEvent @event)
{
//do something
}
}
//事件订阅
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();

6. 跨服务事件消费

在微服务中跨服务事件消费很普遍,这里有一点需要说明的是如果订阅的强类型事件非当前微服务中订阅的事件,需要复制定义订阅的事件类型。换句话说,比如在A服务发布的TestEvent事件,B服务订阅该事件,同样需要在B服务复制定义一个TestEvent

这也是微服务的一个通病,重复代码。

5. 最后

通过一步一步的源码梳理,我们发现eShopOnContainers中事件总线的总体实现思路与引言部分的介绍十分契合。所以对于事件总线,不要觉得高深,明确参与的几个角色以及基本的实现步骤,那么不管是基于RabbitMQ实现也好还是基于Azure Service Bus也好,万变不离其宗!

eShopOnContainers 知多少[5]:EventBus With RabbitMQ的更多相关文章

  1. eShopOnContainers 知多少[1]:总体概览

    引言 在微服务大行其道的今天,Java阵营的Spring Boot.Spring Cloud.Dubbo微服务框架可谓是风水水起,也不得不感慨Java的生态圈的火爆.反观国内.NET阵营,微服务却不愠 ...

  2. eShopOnContainers 知多少[8]:Ordering microservice

    1. 引言 Ordering microservice(订单微服务)就是处理订单的了,它与前面讲到的几个微服务相比要复杂的多.主要涉及以下业务逻辑: 订单的创建.取消.支付.发货 库存的扣减 2. 架 ...

  3. eShopOnContainers 知多少[6]:持久化事件日志

    1. 引言 事件总线解决了微服务间如何基于集成事件进行异步通信的问题.然而只有事件总线正常运行,微服务之间基于事件的通信才得以运转. 而现实情况是,总有这样或那样的问题,导致事件总线不稳定或不可用,比 ...

  4. eShopOnContainers 知多少[4]:Catalog microservice

    引言 Catalog microservice(目录微服务)维护着所有产品信息,包括库存.价格.所以该微服务的核心业务为: 产品信息的维护 库存的更新 价格的维护 架构模式 如上图所示,本微服务采用简 ...

  5. ABP vNext EventBus For RabbitMQ 分布式事件总线使用注意事项_补充官网文档

    [https://docs.abp.io/zh-Hans/abp/latest/Distributed-Event-Bus-RabbitMQ-Integration](ABP vNext官方文档链接) ...

  6. eShopOnContainers 知多少[10]:部署到 K8S | AKS

    1. 引言 断断续续,感觉这个系列又要半途而废了.趁着假期,赶紧再更一篇,介绍下如何将eShopOnContainers部署到K8S上,进而实现大家常说的微服务上云. 2. 先了解下 Helm 读过我 ...

  7. eShopOnContainers 知多少[9]:Ocelot gateways

    引言 客户端与微服务的通信问题永远是一个绕不开的问题,对于小型微服务应用,客户端与微服务可以使用直连的方式进行通信,但对于对于大型的微服务应用我们将不得不面对以下问题: 如何降低客户端到后台的请求数量 ...

  8. eShopOnContainers 知多少[7]:Basket microservice

    引言 Basket microservice(购物车微服务)主要用于处理购物车的业务逻辑,包括: 购物车商品的CRUD 订阅商品价格更新事件,进行购物车商品同步处理 购物车结算事件发布 订阅订单成功创 ...

  9. eShopOnContainers 知多少[3]:Identity microservice

    首先感谢晓晨Master和EdisonChou的审稿!也感谢正在阅读的您! 引言 通常,服务所公开的资源和 API 必须仅限受信任的特定用户和客户端访问.那进行 API 级别信任决策的第一步就是身份认 ...

随机推荐

  1. 解决distinct与order by 的冲突

    sql="select distinct id from test order by otherfield desc" 需要找到不同的id,同时又想让记录按fbsj排序.但是这样一 ...

  2. ZAB协议(Zookeeper atomic Broadcast)

    一.简语: ZAB协议是Paxos算法的经典实现 二.ZAB协议的两种模式: 崩溃恢复: 1.每个server都有一张选票(myid,zxid),选票投给自己 2.收集所有server的投票 3.比较 ...

  3. spring cloud 入门系列六:使用Zuul 实现API网关服务

    通过前面几次的分享,我们了解了微服务架构的几个核心设施,通过这些组件我们可以搭建简单的微服务架构系统.比如通过Spring Cloud Eureka搭建高可用的服务注册中心并实现服务的注册和发现: 通 ...

  4. 修改input获取焦点背景黄色

    input:-webkit-autofill { -webkit-box-shadow: 0 0 0px 1000px white inset !important; }

  5. 如何避免 await/async 地狱

    原文地址:How to escape async/await hell 译文出自:夜色镇歌的个人博客 async/await 把我们从回调地狱中解救了出来,但是如果滥用就会掉进 async/await ...

  6. cocos2d-x 欢乐捕鱼游戏总结

    这几天一直都在做一个捕鱼游戏Demo,大概花掉了我快一个礼拜的时间.游戏主体是使用的cocos2d-x高级开发教程里面提供的小部分框架基本功能.然后自己加入所有的UI元素和玩法.变成了一个体验不错的捕 ...

  7. nfs 、ftp 和samba都有什么区别?

    NFS:Network File System 是已故的Sun公司制定的用于分布式访问的文件系统,它的本质是文件系统.主要在Unix系列操作系统上使用,基于TCP/IP协议层,可以将远程的计算机磁盘挂 ...

  8. C Primer Plus 第6章 C控制语句:循环 编程练习

    记录下写的最后几题. 14. #include <stdio.h> int main() { double value[8]; double value2[8]; int index; f ...

  9. Java如何获取系统信息(包括操作系统、jvm、cpu、内存、硬盘、网络、io等)

    1 下载安装sigar-1.6.4.zip 使用java自带的包获取系统数据,容易找不到包,尤其是内存信息不够准确,所以选择使用sigar获取系统信息. 下载地址:http://sourceforge ...

  10. shell 中各种括号的作用()、(())、[]、[[]]、{}

    一.小括号,圆括号 () 1.单小括号 () 命令组.括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用.括号中多个命令之间用分号隔开,最后一个命令可以没有分号, ...