目录
3.1 安装事件总线Nuget包(Volo.Abp.EventBus.RabbitMQ)
4.1.2 使用IDistributedEventBus发布事件
一、目的
当前公司需要与某公司提供AI分析服务,准备开发一个对外开放平台。整体架构不多赘述。需求大致是:
1、用户通过Api发送分析请求。
2、用户通过Api查询分析结果。
3、提供分析状态查询接口(分析服务消耗时间和资源过大,分析请求排队异步处理)。
这里通过使用
分布式事务总线+Rabbitmq,实现
Api服务和
分析服务之间的通信.
二、什么是事件总线
事件总线是一个调解人,可将消息从发送者传输到接收者。这样,它在对象,服务和应用程序之间提供了一种松散的耦合方式。

2.1 Abp.vnext提供两种事务总线。
本地事件总线:同一进程中,不同方法
过程中的消息传递。
例如:创建账号后,需要发送邮件给用户。如果是单体项目,此时可以把发送邮件功能,通过事务总线,传递给邮件服务进行处理,进行服务解耦。
分布式事件
总线(Distributed Transaction Coordinator, DTC):
是一种用于协调分布式事务的中间件服务。它负责管理跨多个系统或服务的数据库事务,确保事务的一致性和完整性。分布式事务总线通过协调不同系统中的事务执行,确保所有参与的系统在事务提交或回滚时保持一致。
适用于
过程间消息传递,例如在分布式/微服务系统中发布和订阅分布式事件。

2.2 如何选择自己想要使用的事件总线
- 如果您要构建微服务系统,请使用分布式事件总线通信。
- 如果您要发布一个事件并始终以同一微服务进行处理,则可以将本地活动总线用于该事件。
- 如果您要构建模块化整体应用程序,请使用分布式事件总线模块间通信。由于默认情况下,分布式事务总线在过程中工作(除非您配置了真实的分布式事件总线提供商)。这样,如果您以后迁移到微服务系统,这些模块间通信可能会成为无需更改的无代码(仅安装分布式事件总线提供包装包)而成为一种微服务通信。如果您要发布一个事件并始终在同一模块中进行处理,则可以将本地事件总线用于该事件。
- 如果要构建整体(非模块化)应用程序,则可以随时使用本地事件总线。
ps:下图为abp.io官网对于选择的解答。实际上官网强烈推荐的是使用
IDistributedEventBus
分布式事件总线模块间通信.
因为中间件会根据你的实际
配置
进行“本地”和“分布式”事务总线的选择
。

2.3 为什么使用事件总线
- 解耦合(轻松的实现系统间解耦)
- 高性能可扩展(每一个事件都是简单独立且不可更改的对象,只需要保存新增的事件,不涉及其他的变更删除操作)
三、Abp.vnext中配置分布式事务总线
3.1 安装事件总线Nuget包(Volo.Abp.EventBus.RabbitMQ)

3.2 在appsettings.json文件配置
示例:连接到具有默认配置的本地RabbitMQ服务器的最小配置
{
"RabbitMQ": {
"EventBus": {
"ClientName": "MyClientName",//应用程序的名称,用作队列名称在消息队列上
"ExchangeName": "MyExchangeName"//交换机名称
}
}
}
示例:指定主机名(作为IP地址)和
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "ip地址",
"Port": "端口号",
"UserName": "账号",
"Password": "密码",
"VirtualHost": "虚拟主机(服务隔离)"
}
},
"EventBus": {
"ClientName": "Cloud.Api.Result",
"ExchangeName": "Cloud.Ultra.Service.Exchange"
}
},
}
示例:连接到消息队列集群
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "123.123.123.123;234.234.234.234"
}
},
"EventBus": {
"ClientName": "MyClientName",
"ExchangeName": "MyExchangeName"
}
}
}
3.3 应用层添加组件依赖

3.4 配置RabbitMQ的连接字符串和事件总线选项
示例:配置连接
// RabbitMQ连接配置
Configure<AbpRabbitMqOptions>(options =>
{
options.Connections.Default.HostName = configuration["RabbitMQ:Connections:Default:HostName"];
options.Connections.Default.Port = Convert.ToInt32(configuration["RabbitMQ:Connections:Default:Port"]); // 默认端口
options.Connections.Default.UserName = configuration["RabbitMQ:Connections:Default:UserName"];
options.Connections.Default.Password = configuration["RabbitMQ:Connections:Default:Password"];
options.Connections.Default.VirtualHost = configuration["RabbitMQ:Connections:Default:VirtualHost"];
});
3.5 创建事件对象Eto,用于Publish发布
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.EventBus;
namespace AbpDemo.Application.Contracts.Etos
{
/// <summary>
/// 上传分析事件
/// </summary>
[EventName(MqttAnalyseServerEvents.AnalyseUploadClientEventName)]
public class AnalyseUploadEto
{
/// <summary>
/// 唯一编号
/// </summary>
public string Guid { get; set; }
/// <summary>
/// 脏器类型
/// </summary>
public string OrganType { get; set; }
/// <summary>
/// 请求数据
/// </summary>
public string Data { get; set; }
}
}
即使你不需要传输任何数据也需要创建一个类(在这种情况下为空类).
Eto 是我们按照约定使用的
Event
Transfer
Objects(事件传输对象)的后缀. s虽然这不是必需的,但我们发现识别这样的事件类很有用(就像应用层上的
DTO 一样).
ps:事件传输对象
必须是可序列化的,因为将其传输到进程外时,它们将被序列化/反序列化为JSON或其他格式.
避免循环引用,多态,私有setter,并提供默认(空)构造函数,如果你有其他的构造函数.(虽然某些序列化器可能会正常工作),就像DTO一样.
3.6事件名称EventName
EventName
attribute是可选的,但建议使用. 如果不声明,事件名将事件名称将是事件类的全名. 这里是 AbpDemo.AnalyseUpload.Server
.EventName
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AbpDemo.Application.Contracts.Etos
{
public class MqttAnalyseServerEvents
{
#region 统一私有前缀
private const string AnalyseUploadPrefix = "AbpDemo.AnalyseUpload.Server";
private const string AnalyseResultPrefix = "AbpDemo.AnalyseResult.Server";
#endregion
/// <summary>
/// 上传分析事件名
/// </summary>
public const string AnalyseUploadClientEventName = $"{AnalyseUploadPrefix}.EventName";
/// <summary>
/// 分析结果事件名
/// </summary>
public const string AnalyseResultClientEventName = $"{AnalyseResultPrefix}.EventName";
}
}
四、发布和订阅分布式事件总线
4.1 发布分布式事件
4.1.1 在聚合根方法内发布分布式事件
using System;
using Volo.Abp.Domain.Entities;
namespace AbpDemo
{
public class ProductDemo : AggregateRoot<Guid>
{
public string Name { get; set; }
public int StockCount { get; private set; }
private Product() { }
public Product(Guid id, string name)
: base(id)
{
Name = name;
}
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
//ADD an EVENT TO BE PUBLISHED
AddDistributedEvent(
new StockCountChangedEto
{
ProductId = Id,
NewCount = newCount
}
);
}
}
}
AggregateRoot
类定义了
AddDistributedEvent 来添加一个新的分布式事件,事件在聚合根对象保存(创建,更新或删除)到数据库时发布.
如果实体发布这样的事件,以可控的方式更改相关属性是一个好的实践,就像上面的示例一样 - StockCount只能由保证发布事件的 ChangeStockCount 方法来更改.
IGeneratesDomainEvents 接口
实际上添加分布式事件并不是
AggregateRoot 类独有的. 你可以为任何实体类实现
IGeneratesDomainEvents. 但是
AggregateRoot 默认实现了它简化你的工作.
不建议为不是聚合根的实体实现此接口,因为它可能不适用于此类实体的某些数据库提供程序. 例如它适用于EF Core,但不适用于MongoDB.
4.1.2 使用IDistributedEventBus发布事件
using System;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.ObjectMapping;
namespace AbpDemo.BaseService.Application;
public class UploadAppService : ApplicationService, IUploadAppService
{
private readonly IDistributedEventBus _distributedEventBus;
private readonly ILogger<UploadAppService> _logger;
public UploadAppService(IDistributedEventBus distributedEventBus,
ILogger<UploadAppService> logger)
{
_logger = logger;
_distributedEventBus = distributedEventBus;
}
/// <summary>
/// 分布式事件发布
/// </summary>
/// <returns></returns>
public async Task<string> PublishMQAsync(AnalyseUploadEto analyseUploadEto)
{
var result = string.Empty;
try
{
await _distributedEventBus.PublishAsync(analyseUploadEto);
}
catch (Exception ex)
{
_logger.LogError($"异常!Message:{ex.Message} \r\n stack:{ex.StackTrace}");
result = ex.Message;
}
return result;
}
}
4.2 订阅事件
订阅自动事件与订阅常规分布式事件相同.
using AbpDemo.BaseService.Application.Contracts;
using AbpDemo.BaseService.Application.Contracts.Etos;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace AbpDemo.BaseService.Application.Services
{
public class AnalyseResultEventHandler : IDistributedEventHandler<AnalyseResultEto>, ITransientDependency
{
private readonly ILogger<AnalyseResultEventHandler> _logger;
private readonly AnalysisAppService _analysisAppService;
public AnalyseResultEventHandler(AnalysisAppService analysis_THAppService, ILogger<AnalyseResultEventHandler> logger)
{
_analysisAppService = analysisAppService;
_logger = logger;
}
/// <summary>
/// 处理逻辑
/// </summary>
/// <param name="eventData"></param>
/// <returns></returns>
public async Task HandleEventAsync(AnalyseResultEto eventData)
{
try
{
var clientId = eventData.Guid;
//TODO:业务逻辑处理
var res = await _analysisAppService.SaveAnalysis(eventData);
if (!string.IsNullOrEmpty(res))
{
_logger.LogError(res);
}
_logger.LogInformation($"Client:{clientId} has connected the broker");
}
catch (Exception ex)
{
_logger.LogError(ex, "处理分析结果事件异常");
}
}
}
}
- MyHandler 实现了 IDistributedEventHandler>.
五、文章总结
事件总线
能够简化
各组件或进程间
的通信,让我们的代码书写变得简单,能有效的分离事件发送方和接收方(也就是解耦的意思),能避免复杂和容易出错的依赖性和生命周期问题。
把之所学,以文载之。欢迎大家多多交流学习