微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

时间:2022-04-17 23:29:44

eShopOnContainers

eShopOnContainers是微软官方的微服务架构示例,GitHub地址https://github.com/dotnet-architecture/eShopOnContainers

在eShopOnContainers架构中有一个使用RabbitMQ实现的EventBus(事件总线),EventBus使用的是发布订阅模式, 使用事件驱动,使得有了一个新需求后直接添加对应Handler并注册订阅即可,符合单一职责原则。下在就详细说说我是如果把eShopOnContainers中的EventBus用到自己的项目中的

项目结构图

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

持久连接类

面向接口是主流的开发方式,使用rabbitmq我们需要有一个管理与rabbtimq服务器连接的类,也就是

IRabbitMqPersistentConnection 接口与 DefaultRabbitMqPersistentConnection 类

1 /// <summary>

2 /// Rabbitmq持久连接

3 /// </summary>

4 public interface IRabbitMqPersistentConnection : IDisposable

5 {

6 /// <summary>

7 /// 是否连接

8 /// </summary>

9 bool IsConnected { get; }

10

11 /// <summary>

12 /// 尝试连接

13 /// </summary>

14 /// <returns></returns>

15 bool TryConnect();

16

17 IModel CreateModel();

18 }

在接口里定义了三个方法(属性也是方法),第一个判断是否已经连接,第二个尝试连接,第二个方法创建一个channel

在DefaultRabbitMqPersistentConnection类里实现了接口,它需要一个类型为IConnectionFactory的参数

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

首先来看看TryConnect方法

在这里使用了传入的_connectionFactory创建了连接,并且注册了连接成功、失败等事件进行了log输出

1 public bool TryConnect()

2 {

3 Logger.Info("RabbitMQ客户端正在尝试连接");

4

5 lock (_syncRoot)

6 {

7 var policy = Policy.Handle<SocketException>()

8 .Or<BrokerUnreachableException>()

9 .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>

10 {

11 Logger.Warn(ex.ToString());

12 }

13 );

14

15 policy.Execute(() =>

16 {

17 _connection = _connectionFactory

18 .CreateConnection();

19 });

20

21 if (IsConnected)

22 {

23 _connection.ConnectionShutdown += OnConnectionShutdown;

24 _connection.CallbackException += OnCallbackException;

25 _connection.ConnectionBlocked += OnConnectionBlocked;

26

27 Logger.Info($"连接到 {_connection.Endpoint.HostName} 并注册了异常失败事件");

28

29 return true;

30 }

31 Logger.Fatal("致命错误:无法创建和打开RabbitMQ连接");

32

33 return false;

34 }

35 }

订阅管理器

连接类其它的已经没有什么值得一说的了,下面我们把眼光转到订阅管理器

这是管理器的接口,作用是来用维护EventData与EventHandler的关系,图中箭头指向的代码是我自己添加的,是为了定义一个命名约定把EventData与EventHandler的关系统一添加到管理器中

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

订阅管理器接口的默认实现是InMemoryEventBusSubscriptionsManager类放到内存中,也可以轻松添加redis、sql server等实现,InMemoryEventBusSubscriptionsManager内部维护了一个字典,对应关系都放在这个字典中,可以进行添加订阅、取消订阅

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

在说EventBus类之前先说说两种Handler,一种是普通的事件处理,泛型接口接收IntegrationEvent(EventData)的派生类做为参数并且定义了Handler方法来进行处理

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

第二种是动态EventData的Handler,它并不会要求必须是IntegrationEvent的派生类,可以灵活的处理其它的类型

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

事件总线

下面就到了EventBus的接口,定义很简单,进行订阅事件,和dynamic的事件以及取消订阅和最重要的发布事件

1 public interface IEventBus

2 {

3 /// <summary>

4 /// 订阅事件

5 /// </summary>

6 /// <typeparam name="T"></typeparam>

7 /// <typeparam name="TH"></typeparam>

8 void Subscribe<T, TH>()

9 where T : IntegrationEvent

10 where TH : IIntegrationEventHandler<T>;

11

12 /// <summary>

13 /// 订阅动态事件

14 /// </summary>

15 /// <typeparam name="TH"></typeparam>

16 /// <param name="eventName"></param>

17 void SubscribeDynamic<TH>(string eventName)

18 where TH : IDynamicIntegrationEventHandler;

19

20 /// <summary>

21 /// 取消订阅动态事件

22 /// </summary>

23 /// <typeparam name="TH"></typeparam>

24 /// <param name="eventName"></param>

25 void UnsubscribeDynamic<TH>(string eventName)

26 where TH : IDynamicIntegrationEventHandler;

27

28 /// <summary>

29 /// 订阅事件

30 /// </summary>

31 /// <param name="event"></param>

32 /// <param name="handler"></param>

33 void Subscribe(Type @event, Type handler);

34

35 /// <summary>

36 /// 取消订阅事件

37 /// </summary>

38 /// <typeparam name="T"></typeparam>

39 /// <typeparam name="TH"></typeparam>

40 void Unsubscribe<T, TH>()

41 where TH : IIntegrationEventHandler<T>

42 where T : IntegrationEvent;

43

44 /// <summary>

45 /// 发布事件

46 /// </summary>

47 /// <param name="event"></param>

48 void Publish(IntegrationEvent @event);

49 }

我使用的是RabbitMQ的EventBus实现,RabbitMQ是AMQP协议的企业级消息队列,可靠性与性能都非常高。具体大家可以去了解一下RbbaitMQ,

EventBusRabbitMq类需要三个参数

RabbitMqPersistentConnection 持久连接类

IEventBusSubscriptionsManager 订阅管理器

ILifetimeScope 以及autofac (依赖注入)

实际上还需要一个Logger参数,在我的项目中使用了全局的Logger类,所以就不需要这个参数了,在构造函数中可以看到如果没有传入订阅管理器,那么将会使用默认的InMemry,而且在构造函数中就创建了消费者(和Queue), 在示例中是没有常量QueueName的,使用的是断开连接后自动删除的队列,但是这样会丢失消息,所以我改成了持久的队列

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

下面就是创建消费者的方法了,首先声明一个direct类型的交换机,我对方法进行了修改,添加了对消费者的公平分发,以及手动交付和消息持久化来确保消息会被成功的消费,在Received方法中调用了ProcessEvent方法,在这个方法里就是对消息的消费了

1 private IModel CreateConsumerChannel()

2 {

3 if (!_persistentConnection.IsConnected)

4 {

5 _persistentConnection.TryConnect();

6 }

7

8 var channel = _persistentConnection.CreateModel();

9

10 channel.ExchangeDeclare(exchange: BrokerName,

11 type: "direct");

12 //均发,同一时间只处理一个消息

13 channel.BasicQos(0, 1, false);

14 _queueName = channel.QueueDeclare(QueueName, true, false, false);

15

16 var consumer = new EventingBasicConsumer(channel);

17 consumer.Received += async (model, ea) =>

18 {

19 var eventName = ea.RoutingKey;

20 var message = Encoding.UTF8.GetString(ea.Body);

21

22 await ProcessEvent(eventName, message);

23 //交付

24 channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);

25 };

26

27 channel.BasicConsume(queue: _queueName,

28 autoAck: false,

29 consumer: consumer);

30

31 channel.CallbackException += (sender, ea) =>

32 {

33 _consumerChannel.Dispose();

34 _consumerChannel = CreateConsumerChannel();

35 };

36

37 return channel;

38 }

方法很简单,首先判断事件名称在订阅管理器中是否存在,然后创建一个autofac的生命周期

使用EventData的名称拿到所有的Hnalder

var subscriptions = _subsManager.GetHandlersForEvent(eventName);

然后进行判断是否是Dynamic,后面代码基本一致就是从autofac中拿到EventHandler然后调用Handler方法

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

下面就是依赖注入的部分了

首先是注册订阅管理器

在这里创建ConnectionFactory类,并且读取config中的配置项,拿到host、user以及pwd,我又添加了断线重连和工作进程恢复

builder.RegisterInstance(new DefaultRabbitMqPersistentConnection(new ConnectionFactory()

{

HostName = ConfigurationManager.AppSettings["rabbitmqHost"],

UserName = ConfigurationManager.AppSettings["rabbitmqUser"],

Password = ConfigurationManager.AppSettings["rabbitmqPwd"],

//断线重连并恢复工作进程

AutomaticRecoveryEnabled = true,

TopologyRecoveryEnabled = true

})).As<IRabbitMqPersistentConnection>()

.SingleInstance();

然后就是注册事件总统与订阅管理器

builder.RegisterType<EventBusRabbitMq.EventBusRabbitMq>().As<IEventBus>().SingleInstance();

builder.RegisterType<InMemoryEventBusSubscriptionsManager>().As<IEventBusSubscriptionsManager>().SingleInstance();

最后我拿到了EventData和EventHandler所在程序中,扫描所有的Type并为所有以Handler结尾的类型添加到容器了,然后拿到了派生于IntegrationEvent的所有子类然后与对应的Handler进行订阅,这样基本上大多数的开发场景都不需要单独进行订阅了,只要符合命名约定就会自动订阅

//配置EventBus

var ass = Assembly.GetAssembly(typeof(Response));

builder.RegisterAssemblyTypes(ass).Where(t => t.Name.EndsWith("Handler")).InstancePerDependency();

var build = builder.Build();

var bus = build.Resolve<IEventBus>();

var allEvent = ass.GetTypes().Where(o => o.IsSubclassOf(typeof(IntegrationEvent)));

foreach (var t in allEvent)

{

var handler = ass.GetTypes().FirstOrDefault(o => o.Name.StartsWith(t.Name) && o.Name.EndsWith("Handler"));

if (handler != null)

bus.Subscribe(t, handler);

}

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践

最后就是使用了,我定义了TestEvent与TestEventHandler类,在Handler中输出TestEvent的属性 Test

1 public class TestEvent : IntegrationEvent

2 {

3 public string Test { get; set; }

4 }

5

6 public class TestEventHandler : IIntegrationEventHandler<TestEvent>

7 {

8 private readonly Repository<Lawfirm> _lawfirmRepository;

9

10 public TestEventHandler(Repository<Lawfirm> lawfirmRepository)

11 {

12 _lawfirmRepository = lawfirmRepository;

13 }

14

15 public Task Handle(TestEvent @event)

16 {

17 Console.WriteLine(@event.Test);

18 return Task.FromResult(0);

19 }

20 }

使用非常简单,下面我使用事件总线发布了一个消息,然后把程序运行起来

1 _eventBus = container.Resolve<IEventBus>();

2

3 _eventBus.Publish(new TestEvent

4 {

5 Test = "Hello World"

6 });

我们看到了刚才发布的消息HelloWorld ,哈哈,输出是不是很奇怪,不家什么成功导入x条,这是因为我的爬虫项目,在博文的同时正在采集着数据,消息被发到了这个消费者下并且被成功消费。安利一下我搭的爬虫框架,使用了.net core 2.0+Ef core 2.0,集成了无头浏览器与anglesharp还有EventBus那就不用说了,相关技术栈有 依赖注入、仓储模式、AutoMapper、还有一个Web的管理界面以及下一步要集成的Hangfire任务调度。有感兴趣的可以留言,我会根据反馈信息决定是否开源分享给大家

微软微服务eShopOnContainers示例之EventBusRabbitMq解析与实践