首先我們需要了解到分散式事件匯流排是什麼;
分散式事件匯流排是一種在分散式系統中提供事件通知、訂閱和釋出機制的技術。它允許多個元件或微服務之間的協作和通訊,而無需直接耦合或瞭解彼此的實現細節。透過事件匯流排,元件或微服務可以透過釋出或訂閱事件來實現非同步通訊。
例如,當一個元件完成了某項任務並生成了一個事件,它可以透過事件匯流排釋出該事件。其他相關元件可以透過訂閱該事件來接收通知,並做出相應的反應。這樣,元件之間的耦合就被減輕了,同時也提高了系統的可維護性和可擴充套件性。
然後瞭解一下RabbitMQ
RabbitMQ
是一種開源的訊息代理和佇列管理系統,用於在分散式系統中進行非同步通訊。它的主要功能是接收和分發訊息,並且支援多種協議,包括AMQP,STOMP,MQTT等。RabbitMQ
透過一箇中間層,可以把訊息傳送者與訊息接收者隔離開來,因此訊息傳送者和訊息接收者並不需要在同一時刻線上,並且也不需要互相知道對方的地址。
- RabbitMQ的主要功能包括:
- 訊息儲存:RabbitMQ可以將訊息儲存在記憶體或硬碟上,以保證訊息的完整性。
- 訊息路由:RabbitMQ支援訊息的路由功能,可以將訊息從生產者傳送到消費者。
- 訊息投遞:RabbitMQ提供了多種訊息投遞策略,包括簡單模式、工作佇列、釋出/訂閱模式等。
- 可靠性:RabbitMQ保證訊息的可靠性,即訊息不會丟失、不重複、按順序投遞。
- 可擴充套件性: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分散式事件匯流排的示例
首先我們需要準備一個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