分布式事件总线是一种在分布式系统中提供事件通知、订阅和发布机制的技术。它允许多个组件或微服务之间的协作和通信,而无需直接耦合或了解彼此的实现细节。通过事件总线,组件或微服务可以通过发布或订阅事件来实现异步通信。
例如,当一个组件完成了某项任务并生成了一个事件,它可以通过事件总线发布该事件。其他相关组件可以通过订阅该事件来接收通知,并做出相应的反应。这样,组件之间的耦合就被减轻了,同时也提高了系统的可维护性和可扩展性。
RabbitMQRabbitMQ是一种开源的消息代理和队列管理系统,用于在分布式系统中进行异步通信。它的主要功能是接收和分发消息,并且支持多种协议,包括AMQP,STOMP,MQTT等。RabbitMQ通过一个中间层,可以把消息发送者与消息接收者隔离开来,因此消息发送者和消息接收者并不需要在同一时刻在线,并且也不需要互相知道对方的地址。
本文将讲解使用RabbitMQ实现分布式事件
实现我们创建一个EventsBus.Contract的类库项目,用于提供基本接口,以支持其他实现
在项目中添加以下依赖引用,并且记得添加EventsBus.Contract项目引用
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="RabbitMQ.Client" Version="6.4.0" />
</ItemGroup>
创建项目完成以后分别创建EventsBusOptions.cs,IEventsBusHandle.cs,RabbitMQEventsManage.cs,ILoadEventBus.cs ,提供我们的分布式事件基本接口定义
EventsBusOptions.cs:
namespace EventsBus.Contract;
public class EventsBusOptions
{
/// <summary>
/// 接收时异常事件
/// </summary>
public static Action<IServiceProvider, Exception,byte[]>? ReceiveExceptionEvent;
}
IEventsBusHandle.cs:
namespace EventsBus.Contract;
public interface IEventsBusHandle<in TEto> where TEto : class
{
Task HandleAsync(TEto eventData);
}
ILoadEventBus.cs:
namespace EventsBus.Contract;
public interface ILoadEventBus
{
/// <summary>
/// 发布事件
/// </summary>
/// <param name="eto"></param>
/// <typeparam name="TEto"></typeparam>
/// <returns></returns>
Task PushAsync<TEto>(TEto eto) where TEto : class;
}
EventsBusAttribute.cs:用于Eto(Eto 是我们按照约定使用的Event Transfer Objects(事件传输对象)的后缀. s虽然这不是必需的,但我们发现识别这样的事件类很有用(就像应用层上的DTO 一样))的名称,对应到RabbitMQ的通道
namespace EventsBus.RabbitMQ;
[AttributeUsage(AttributeTargets.Class)]
public class EventsBusAttribute : Attribute
{
public readonly string Name;
public EventsBusAttribute(string name)
{
Name = name;
}
}
然后可以创建我们的RabbitMQ实现了,创建EventsBus.RabbitMQ类库项目,用于编写EventsBus.Contract的RabbitMQ实现
创建项目完成以后分别创建Extensions\EventsBusRabbitMQExtensions.cs,Options\RabbitMQOptions.cs,EventsBusAttribute.cs,,RabbitMQFactory.cs,RabbitMQLoadEventBus.cs
Extensions\EventsBusRabbitMQExtensions.cs:提供我们RabbitMQ扩展方法让使用者更轻松的注入,命名空间使用Microsoft.Extensions.DependencyInjection,这样就在注入的时候减少过度使用命名空间了
using EventsBus.Contract;
using EventsBus.RabbitMQ;
using EventsBus.RabbitMQ.Options;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection;
public static class EventsBusRabbitMQExtensions
{
public static IServiceCollection AddEventsBusRabbitMQ(this IServiceCollection services,
IConfiguration configuration)
{
services.AddSingleton<RabbitMQFactory>();
services.AddSingleton(typeof(RabbitMQEventsManage<>));
services.Configure<RabbitMQOptions>(configuration.GetSection(nameof(RabbitMQOptions)));
services.AddSingleton<ILoadEventBus, RabbitMQLoadEventBus>();
return services;
}
}
Options\RabbitMQOptions.cs:提供基本的Options 读取配置文件中并且注入,services.Configure<RabbitMQOptions>(configuration.GetSection(nameof(RabbitMQOptions)));的方法是读取IConfiguration的名称为RabbitMQOptions的配置东西,映射到Options中,具体使用往下看。
using RabbitMQ.Client;
namespace EventsBus.RabbitMQ.Options;
public class RabbitMQOptions
{
/// <summary>
/// 要连接的端口。 <see cref="AmqpTcpEndpoint.UseDefaultPort"/>
/// 指示应使用的协议的缺省值。
/// </summary>
public int Port { get; set; } = AmqpTcpEndpoint.UseDefaultPort;
/// <summary>
/// 地址
/// </summary>
public string HostName { get; set; }
/// <summary>
/// 账号
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; }
}
RabbitMQEventsManage.cs:用于管理RabbitMQ的数据接收,并且将数据传输到指定的事件处理程序
using System.Reflection;
using System.Text.Json;
using EventsBus.Contract;
using Microsoft.Extensions.DependencyInjection;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace EventsBus.RabbitMQ;
public class RabbitMQEventsManage<TEto> where TEto : class
{
private readonly IServiceProvider _serviceProvider;
private readonly RabbitMQFactory _rabbitMqFactory;
public RabbitMQEventsManage(IServiceProvider serviceProvider, RabbitMQFactory rabbitMqFactory)
{
_serviceProvider = serviceProvider;
_rabbitMqFactory = rabbitMqFactory;
_ = Task.Run(Start);
}
private void Start()
{
var channel = _rabbitMqFactory.CreateRabbitMQ();
var eventBus = typeof(TEto).GetCustomAttribute<EventsBusAttribute>();
var name = eventBus?.Name ?? typeof(TEto).Name;
channel.QueueDeclare(name, false, false, false, null);
var consumer = new EventingBasicConsumer(channel); //消费者
channel.BasicConsume(name, true, consumer); //消费消息
consumer.Received += async (model, ea) =>
{
var bytes = ea.Body.ToArray();
try
{
// 这样就可以实现多个订阅
var events = _serviceProvider.GetServices<IEventsBusHandle<TEto>>();
foreach (var handle in events)
{
await handle?.HandleAsync(JsonSerializer.Deserialize<TEto>(bytes));
}
}
catch (Exception e)
{
EventsBusOptions.ReceiveExceptionEvent?.Invoke(_serviceProvider, e, bytes);
}
};
}
}
RabbitMQFactory.cs:提供RabbitMQ链接工厂,在这里你可以自己去定义和管理RabbitMQ工厂
using EventsBus.RabbitMQ.Options;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
namespace EventsBus.RabbitMQ;
public class RabbitMQFactory : IDisposable
{
private readonly RabbitMQOptions _options;
private readonly ConnectionFactory _factory;
private IConnection? _connection;
public RabbitMQFactory(IOptions<RabbitMQOptions> options)
{
_options = options?.Value;
// 将Options中的参数添加到ConnectionFactory
_factory = new ConnectionFactory
{
HostName = _options.HostName,
UserName = _options.UserName,
Password = _options.Password,
Port = _options.Port
};
}
public IModel CreateRabbitMQ()
{
// 当第一次创建RabbitMQ的时候进行链接
_connection ??= _factory.CreateConnection();
return _connection.CreateModel();
}
public void Dispose()
{
_connection?.Dispose();
}
}
RabbitMQLoadEventBus.cs:用于实现ILoadEventBus.cs通过ILoadEventBus发布事件RabbitMQLoadEventBus.cs是RabbitMQ的实现
using System.Reflection;
using System.Text.Json;
using EventsBus.Contract;
using Microsoft.Extensions.DependencyInjection;
namespace EventsBus.RabbitMQ;
public class RabbitMQLoadEventBus : ILoadEventBus
{
private readonly IServiceProvider _serviceProvider;
private readonly RabbitMQFactory _rabbitMqFactory;
public RabbitMQLoadEventBus(IServiceProvider serviceProvider, RabbitMQFactory rabbitMqFactory)
{
_serviceProvider = serviceProvider;
_rabbitMqFactory = rabbitMqFactory;
}
public async Task PushAsync<TEto>(TEto eto) where TEto : class
{
//创建一个通道
//这里Rabbit的玩法就是一个通道channel下包含多个队列Queue
using var channel = _rabbitMqFactory.CreateRabbitMQ();
// 获取Eto中的EventsBusAttribute特性,获取名称,如果没有默认使用类名称
var eventBus = typeof(TEto).GetCustomAttribute<EventsBusAttribute>();
var name = eventBus?.Name ?? typeof(TEto).Name;
// 使用获取的名称创建一个通道
channel.QueueDeclare(name, false, false, false, null);
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 1;
// 将数据序列号,然后发布
channel.BasicPublish("", name, false, properties, JsonSerializer.SerializeToUtf8Bytes(eto)); //生产消息
// 让其注入启动管理服务,RabbitMQEventsManage需要手动激活,由于RabbitMQEventsManage是单例,只有第一次激活才有效,
var eventsManage = _serviceProvider.GetService<RabbitMQEventsManage<TEto>>();
await Task.CompletedTask;
}
}
在这里我们的RabbitMQ分布式事件就设计完成了,注:这只是简单的一个示例,并未经过大量测试,请勿直接在生产使用;
然后我们需要使用RabbitMQ分布式事件总线工具包
首先我们需要准备一个RabbitMQ,可以在官网自行下载,我就先使用简单的,通过docker compose启动一个RabbitMQ,下面提供一个compose文件
version: '3.1'
services:
rabbitmq:
restart: always # 开机自启
image: rabbitmq:3.11-management # RabbitMQ使用的镜像
container_name: rabbitmq # docker名称
hostname: rabbit
ports:
- 5672:5672 # 只是RabbitMQ SDK使用的端口
- 15672:15672 # 这是RabbitMQ管理界面使用的端口
environment:
TZ: Asia/Shanghai # 设置RabbitMQ时区
RABBITMQ_DEFAULT_USER: token # rabbitMQ账号
RABBITMQ_DEFAULT_PASS: dd666666 # rabbitMQ密码
volumes:
- ./data:/var/lib/rabbitmq
启动以后我们创建一个WebApi项目,项目名称Demo,创建完成打开项目文件添加引用
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<!-- 引用RabbitMQ事件总线项目-->
<ProjectReference Include="..\EventsBus.RabbitMQ\EventsBus.RabbitMQ.csproj" />
</ItemGroup>
</Project>
修改appsettings.json配置文件:将RabbitMQ的配置写上,RabbitMQOptions名称对应在EventsBus.RabbitMQ中的RabbitMQOptions文件![image-20230211022801105]
在这里注入的时候将配置注入好了
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"RabbitMQOptions": {
"HostName": "127.0.0.1",
"UserName": "token",
"Password": "dd666666"
}
}
创建DemoEto.cs文件:
using EventsBus.RabbitMQ;
namespace Demo;
[EventsBus("Demo")]
public class DemoEto
{
public int Size { get; set; }
public string Value { get; set; }
}
创建DemoEventsBusHandle.cs文件:这里是订阅DemoEto事件,相当于是DemoEto的处理程序
using System.Text.Json;
using EventsBus.Contract;
namespace Demo;
/// <summary>
/// 事件处理服务,相当于订阅事件
/// </summary>
public class DemoEventsBusHandle : IEventsBusHandle<DemoEto>
{
public async Task HandleAsync(DemoEto eventData)
{
Console.WriteLine($"DemoEventsBusHandle: {JsonSerializer.Serialize(eventData)}");
await Task.CompletedTask;
}
}
打开Program.cs 修改代码: 在这里注入了事件总线服务,和我们的事件处理服务
using Demo;
using EventsBus.Contract;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 注入事件处理服务
builder.Services.AddSingleton(typeof(IEventsBusHandle<DemoEto>),typeof(DemoEventsBusHandle));
// 注入RabbitMQ服务
builder.Services.AddEventsBusRabbitMQ(builder.Configuration);
var app = builder.Build();
// 只有在Development显示Swagger
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 强制Https
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
创建Controllers\EventBusController.cs控制器:我们在控制器中注入了ILoadEventBus ,通过调用接口实现发布事件;
using EventsBus.Contract;
using Microsoft.AspNetCore.Mvc;
namespace Demo.Controllers;
[ApiController]
[Route("[controller]")]
public class EventBusController : ControllerBase
{
private readonly ILoadEventBus _loadEventBus;
public EventBusController(ILoadEventBus loadEventBus)
{
_loadEventBus = loadEventBus;
}
/// <summary>
/// 发送信息
/// </summary>
/// <param name="eto"></param>
[HttpPost]
public async Task Send(DemoEto eto)
{
await _loadEventBus.PushAsync(eto);
}
}
然后我们启动程序会打开Swagger调试界面:

然后我们发送一下事件:

我们可以看到,在数据发送的时候也同时订阅到了我们的信息,也可以通过分布式事件总线限流等实现,
来自Token的分享
技术交流群:737776595
我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和
是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s
我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).
这是我在ActiveAdmin中的自定义页面ActiveAdmin.register_page"Settings"doaction_itemdolink_to('Importprojects','settings/importprojects')endcontentdopara"Text"endcontrollerdodefimportprojectssystem"rakedataspider:import_projects_ninja"para"OK"endendend我想做的是,当我单击“导入项目”按钮时,我想在Controller中执行rake任务。但是我无法访问该方法。可能是什
例如,假设我有一个名为Products的模型,并且在ProductsController中,我有以下代码用于product_listView以显示已排序的产品。@products=Product.order(params[:order_by])让我们想象一下,在product_listView中,用户可以使用下拉菜单按价格、评级、重量等进行排序。数据库中的产品不会经常更改。我很难理解的是,每次用户选择新的order_by过滤器时,rails是否必须查询,或者rails是否能够以某种方式缓存事件记录以在服务器端重新排序?有没有一种方法可以编写它,以便在用户排序时rails不会重新查询结果
我有一个将某些事件写入队列的Rails3应用。现在我想在服务器上创建一个服务,每x秒轮询一次队列,并按计划执行其他任务。除了创建ruby脚本并通过cron作业运行它之外,还有其他稳定的替代方案吗? 最佳答案 尽管启动基于Rails的持久任务是一种选择,但您可能希望查看更有序的系统,例如delayed_job或Starling管理您的工作量。我建议不要在cron中运行某些东西,因为启动整个Rails堆栈的开销可能很大。每隔几秒运行一次它是不切实际的,因为Rails上的启动时间通常为5-15秒,具体取决于您的硬件。不过,每天这样做几
我有一个帖子属于城市的关系,城市又属于一个州,例如:classPost现在我想找到所有帖子及其所属的城市和州。我编写了以下查询来获取带有城市的帖子,但不知道如何在同一查找器中获取带有城市的相应州:@post=Post.find:all,:include=>[:city]感谢任何帮助。谢谢。 最佳答案 Post.all(:include=>{:city=>:state}) 关于ruby-on-rails-使用Rails事件记录获取二级模型,我们在StackOverflow上找到一个类似的问
我觉得我错过了什么。我正在编写一个rubygem,它允许与事件记录进行交互,作为其主要功能的附加功能。在为其编写测试用例时,我需要能够指定虚拟事件记录模型来测试此功能。如果我可以获得一个事件记录模型的实例,它不需要与数据库的任何连接,可以有关系,所有这些东西,但不需要我在数据库中设置表,那就太棒了。我对测试还很陌生,在Rails测试之外我也很陌生,但似乎我应该能够相当轻松地完成类似的事情,但我什么也没找到。谁能告诉我我错过了什么?我看过工厂、制造商、固定装置,所有这些似乎都想达到目标。人们如何在您只需要AR对象进行测试的地方测试gem? 最佳答案
我想创建一个模块,为从事件记录库继承的类提供一些通用方法。以下是我们可以实现的两种方式。1)moduleCommentabledefself.extended(base)base.class_evaldoincludeInstanceMethodsextendClassMethodsendendmoduleClassMethodsdeftest_commentable_classmethodputs'testclassmethod'endendmoduleInstanceMethodsdeftest_commentable_instance_methodputs'testinstanc
我爱Sanitize.这是一个了不起的实用程序。我遇到的唯一问题是,它需要永远准备一个开发环境,因为它使用Nokogiri,这对编译时间来说是一种痛苦。是否有任何程序可以在不使用Nokogiri的情况下执行Sanitize的操作(如果没有别的,只是温和地执行它的操作)?这将以指数方式提供帮助! 最佳答案 Rails有自己的SanitizeHelper。根据http://api.rubyonrails.org/classes/ActionView/Helpers/SanitizeHelper.html,它将Thissanitizehe