ABP - 事件总线之分布式事件总线
事件总线可以实现代码逻辑的解耦,使代码模块之间功能职责更清晰。而分布式事件总线的功能不止这些,它允许我们通过消息队列进行中转,发布和订阅跨应用/服务边界传输的事件,经常被用作微服务或不同应用程序之间异步发送和接收消息的手段,属于分布式应用通讯的方式之一。
分布式事件总线依赖于 消息队列 中间件,ABP 框架中提供了4种开箱即用的提供程序,我们也可以基于抽象的接口自行实现分布式事件总线提供程序,已有的四种分别适用于 RabbitMQ、Kafka、Rebus 等消息队列,还有一种是默认实现:进程内的分布式事件总线,它允许我们在没有接入消息队列时,也能够编写与分布式体系结构兼容的代码,方便日后可能的微服务拆分,这时它的工作方式与本地事件总线一样,整体的设计思想就和微软的分布式缓存一样。
1. 分布式事件总线的集成
以下的演示还是基于控制台程序,分布式事件总线不会默认集成在 ABP 启动模板之中,需要我们自行集成,Web 应用的集成方式也是一样的。
通过以下命令创建一个控制台程序启动模板:
abp new AbpDistributeEventBusSample -t console
之后再打开解决方案,由于分布式事件总线是在不同应用程序之间进行通讯的,所以还需要再创建一个控制台项目进行演示,将解决方案中的项目复制一份即可。
本地分布式事件总线
首先讲一下进程内的事件总线的集成,这个还是有些必要的,如果有考虑后续进行微服务拆分的情况下,前期对于事件总线的使用可以基于这个进行开发。当然并不是说本地事件总线就不推荐使用,从我自己的日常工作经验中,很多时候还是分布式事件总线和本地事件总线搭配使用的。
首先,分布式事件总线的核心依赖包为 Volo.Abp.EventBus,和本地事件总线一样。我们在需要集成的项目的根目录下,通过以下命令进行集成:
abp add-package Volo.Abp.EventBus
由于是本地分布式事件总线,所以这种方式下是没办法跨进程通讯的,使用方式和上一篇讲的本地事件总线类似,不过使用的时候不再通过 ILocalEventBus接口,而是通过 IDistributedEventBus 接口,主要是用于业务逻辑的解耦,同时也为后续可能的分布式拆分做好准备。具体的使用方式下面细讲。
1.2 基于 RabbitMQ 的分布式事件总线
ABP 框架提供了三种开箱即用的分布式事件总线提供程序,分别对应 RabbitMQ、Kafka、Rebus,通过结合第三方消息队列实现真正基于消息跨进程通讯的分布式事件总线,这里主要讲一下基于 RabbitMQ 的方式,其他方式用法类似。
首先,基于 RabbitMQ 的分布式事件总线需要安装 Volo.Abp.EventBus.RabbitMQ 驱动程序包。可通过一下方式安装:
abp add-package Volo.Abp.EventBus.RabbitMQ
上面创建的两个工程都要安装,因为我们要演示两个进程间的通讯。
之后,需要部署 RabbitMQ 消息队列,这里我通过 docker 快速启动一个带有管理平台的 RabbitMQ,命令如下:
docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq rabbitmq:management
RabbitMQ 默认用户密码为:guest / guest,这里只是用于测试就直接使用了。生产环境中大家最好将默认用户禁用,另行创建自己的用户。RabbitMQ 相关的更详细的使用和配置这里就不细讲了,详细内容可见 [[2.1 RabbitMQ基本概念]] 系列文章。
然后,添加分布式事件总线相应的配置,我们可以在 appsettings.json 文件中添加以下配置节点:
"RabbitMQ": {
"Connections": {
// 这里的配置支持 RabbitMQ 官方 sdk 中的 ConnectionFactory 的任意属性的配置
"Default": {
"HostName": "localhost", // rabbitmq 地址,集群环境下多个ip用逗号分隔
"Port": "5672", // rabbbitmq 端口,默认5672
"UserName": "guest",
"Password": "guest"
}
// 允许配置多个 rabbitmq 连接,但只能有一个用于事件总线
//"Second": {
// "HostName": "xxx.xxx.xxx.xxx", // rabbitmq 地址,集群环境下多个ip用逗号分隔
// "Port": "5672" // rabbbitmq 端口,默认5672
//}
},
"EventBus": {
"ClientName": "MyClientName", // 用于事件总线的队列名
"ExchangeName": "MyExchangeName", // 用于事件总线的交换机名称
// "ConnectionName": "Default" // 配置多个连接时,指定用于事件总线的 RabbitMQ 连接,默认是 Default
}
}
以上配置,最后都会被转换为 AbpRabbitMqOptions 和 AbpRabbitMqEventBusOptions,所以我们也可以直接在代码中对这两个选项进行配置:
Configure<AbpRabbitMqOptions>(options =>
{
options.Connections.Default.UserName = "guest";
options.Connections.Default.Password = "guest";
options.Connections.Default.HostName = "localhost";
options.Connections.Default.Port = 5672;
});
Configure<AbpRabbitMqEventBusOptions>(options =>
{
options.ClientName = "TestApp1";
options.ExchangeName = "TestMessages";
});
两种方式选择一种即可,如果两种方式同时使用,代码配置优先于配置文件。解决方案的两个项目都需要进行配置,进行通讯的两个项目需要连接到同一个队列。
完成上面的配置之后,启动应用,即可看到 RabbitMQ 中创建了我们配置的交换机和队列:
2. 分布式事件总线的使用
2.1 发布
事件发布需要一个事件对象,官方将之称为 Eto(事件传输对象),这是一个普通类,用于保存和事件相关的数据,一般以 Eto 作为后缀。就算一个事件不需要传输任何数据,也必须创建一个空类,这和上一章的本地事件总线是一样的,由于在分布式事件触发之后,事件对象会被序列化传输到消息队列中,所以事件对象应避免循环引用、多态、私有setter,并提供默认(空)构造函数,如果你有其他的构造函数(虽然某些序列化器可能会正常工作)。 下面是一个用于测试的 Eto 对象的定义。
[EventName("helloEvent")]
public class HelloEto
{
public string Who {
get; set; }
public DateTime When {
get; set; }
public string ToWho {
get; set; }
}
默认情况下,事件名将事件名称将是事件类的全名,我们可以通过 EventNameattribute 特性指定事件名称。
分布式事件的发布通过 IDistributedEventBus 接口,只需将其注入到相应的类中使用即可,使用方式和本地事件总线一样。
public