动手造轮子:实现一个简单的 EventBus

Intro

EventBus 是一种事件发布订阅模式,通过 EventBus 我们可以很方便的实现解耦,将事件的发起和事件的处理的很好的分隔开来,很好的实现解耦。 微软官方的示例项目 EShopOnContainers 也有在使用 EventBus 。

这里的 EventBus 实现也是参考借鉴了微软 eShopOnContainers 项目。

EventBus 处理流程:

微服务间使用 EventBus 实现系统间解耦:

借助 EventBus 我们可以很好的实现组件之间,服务之间,系统之间的解耦以及相互通信的问题。

起初觉得 EventBus 和 MQ 其实差不多嘛,都是通过异步处理来实现解耦合,高性能。后来看到了下面这张图才算明白为什么要用 EventBus 以及 EventBus 和 MQ 之间的关系,EventBus 是抽象的,可以用MQ来实现 EventBus.

为什么要使用 EventBus

  1. 解耦合(轻松的实现系统间解耦)
  2. 高性能可扩展(每一个事件都是简单独立且不可更改的对象,只需要保存新增的事件,不涉及其他的变更删除操作)
  3. 系统审计(每一个事件都是不可变更的,每一个事件都是可追溯的)
  4. ...

EventBus 整体架构:

  • IEventBase :所有的事件应该实现这个接口,这个接口定义了事件的唯一id EventId 和事件发生的事件 EventAt

  • IEventHandler:定义了一个 Handle 方法来处理相应的事件

  • IEventStore:所有的事件的处理存储,保存事件的IEventHandler,一般不会直接操作,通过 EventBus 的订阅和取消订阅来操作 EventStore

  • IEventBus:用来发布/订阅/取消订阅事件,并将事件的某一个 IEventHandler 保存到 EventStore 或从 EventStore 中移除

使用示例

来看一个使用示例,完整代码示例:

internal class EventTest
{
public static void MainTest()
{
var eventBus = DependencyResolver.Current.ResolveService<IEventBus>();
eventBus.Subscribe<CounterEvent, CounterEventHandler1>();
eventBus.Subscribe<CounterEvent, CounterEventHandler2>();
eventBus.Subscribe<CounterEvent, DelegateEventHandler<CounterEvent>>();
eventBus.Publish(new CounterEvent { Counter = 1 }); eventBus.Unsubscribe<CounterEvent, CounterEventHandler1>();
eventBus.Unsubscribe<CounterEvent, DelegateEventHandler<CounterEvent>>();
eventBus.Publish(new CounterEvent { Counter = 2 });
}
} internal class CounterEvent : EventBase
{
public int Counter { get; set; }
} internal class CounterEventHandler1 : IEventHandler<CounterEvent>
{
public Task Handle(CounterEvent @event)
{
LogHelper.GetLogger<CounterEventHandler1>().Info($"Event Info: {@event.ToJson()}, Handler Type:{GetType().FullName}");
return Task.CompletedTask;
}
} internal class CounterEventHandler2 : IEventHandler<CounterEvent>
{
public Task Handle(CounterEvent @event)
{
LogHelper.GetLogger<CounterEventHandler2>().Info($"Event Info: {@event.ToJson()}, Handler Type:{GetType().FullName}");
return Task.CompletedTask;
}
}

具体实现

EventStoreInMemory 实现:

EventStoreInMemory 是 IEventStore 将数据放在内存中的实现,使用了 ConcurrentDictionary 以及 HashSet 来尽可能的保证高效,具体实现代码如下:

public class EventStoreInMemory : IEventStore
{
private readonly ConcurrentDictionary<string, HashSet<Type>> _eventHandlers = new ConcurrentDictionary<string, HashSet<Type>>(); public bool AddSubscription<TEvent, TEventHandler>()
where TEvent : IEventBase
where TEventHandler : IEventHandler<TEvent>
{
var eventKey = GetEventKey<TEvent>();
if (_eventHandlers.ContainsKey(eventKey))
{
return _eventHandlers[eventKey].Add(typeof(TEventHandler));
}
else
{
return _eventHandlers.TryAdd(eventKey, new HashSet<Type>()
{
typeof(TEventHandler)
});
}
} public bool Clear()
{
_eventHandlers.Clear();
return true;
} public ICollection<Type> GetEventHandlerTypes<TEvent>() where TEvent : IEventBase
{
if(_eventHandlers.Count == 0)
return new Type[0];
var eventKey = GetEventKey<TEvent>();
if (_eventHandlers.TryGetValue(eventKey, out var handlers))
{
return handlers;
}
return new Type[0];
} public string GetEventKey<TEvent>()
{
return typeof(TEvent).FullName;
} public bool HasSubscriptionsForEvent<TEvent>() where TEvent : IEventBase
{
if(_eventHandlers.Count == 0)
return false; var eventKey = GetEventKey<TEvent>();
return _eventHandlers.ContainsKey(eventKey);
} public bool RemoveSubscription<TEvent, TEventHandler>()
where TEvent : IEventBase
where TEventHandler : IEventHandler<TEvent>
{
if(_eventHandlers.Count == 0)
return false; var eventKey = GetEventKey<TEvent>();
if (_eventHandlers.ContainsKey(eventKey))
{
return _eventHandlers[eventKey].Remove(typeof(TEventHandler));
}
return false;
}
}

EventBus 的实现,从上面可以看到 EventStore 保存的是 IEventHandler 对应的 Type,在 Publish 的时候根据 Type 从 IoC 容器中取得相应的 Handler 即可,如果没有在 IoC 容器中找到对应的类型,则会尝试创建一个类型实例,然后调用 IEventHandler 的 Handle 方法,代码如下:

/// <summary>
/// EventBus in process
/// </summary>
public class EventBus : IEventBus
{
private static readonly ILogHelperLogger Logger = Helpers.LogHelper.GetLogger<EventBus>(); private readonly IEventStore _eventStore;
private readonly IServiceProvider _serviceProvider; public EventBus(IEventStore eventStore, IServiceProvider serviceProvider = null)
{
_eventStore = eventStore;
_serviceProvider = serviceProvider ?? DependencyResolver.Current;
} public bool Publish<TEvent>(TEvent @event) where TEvent : IEventBase
{
if (!_eventStore.HasSubscriptionsForEvent<TEvent>())
{
return false;
}
var handlers = _eventStore.GetEventHandlerTypes<TEvent>();
if (handlers.Count > 0)
{
var handlerTasks = new List<Task>();
foreach (var handlerType in handlers)
{
try
{
if (_serviceProvider.GetServiceOrCreateInstance(handlerType) is IEventHandler<TEvent> handler)
{
handlerTasks.Add(handler.Handle(@event));
}
}
catch (Exception ex)
{
Logger.Error(ex, $"handle event [{_eventStore.GetEventKey<TEvent>()}] error, eventHandlerType:{handlerType.FullName}");
}
}
handlerTasks.WhenAll().ConfigureAwait(false); return true;
}
return false;
} public bool Subscribe<TEvent, TEventHandler>()
where TEvent : IEventBase
where TEventHandler : IEventHandler<TEvent>
{
return _eventStore.AddSubscription<TEvent, TEventHandler>();
} public bool Unsubscribe<TEvent, TEventHandler>()
where TEvent : IEventBase
where TEventHandler : IEventHandler<TEvent>
{
return _eventStore.RemoveSubscription<TEvent, TEventHandler>();
}
}

项目实例

来看一个实际的项目中的使用,在我的活动室预约项目中有一个公告的模块,访问公告详情页面,这个公告的访问次数加1,把这个访问次数加1改成了用 EventBus 来实现,实际项目代码:https://github.com/WeihanLi/ActivityReservation/blob/67e2cb8e92876629a7af6dc051745dd8c7e9faeb/ActivityReservation/Startup.cs

  1. 定义 Event 以及 EventHandler
public class NoticeViewEvent : EventBase
{
public Guid NoticeId { get; set; } // UserId
// IP
// ...
} public class NoticeViewEventHandler : IEventHandler<NoticeViewEvent>
{
public async Task Handle(NoticeViewEvent @event)
{
await DependencyResolver.Current.TryInvokeServiceAsync<ReservationDbContext>(async dbContext =>
{
var notice = await dbContext.Notices.FindAsync(@event.NoticeId);
notice.NoticeVisitCount += 1;
await dbContext.SaveChangesAsync();
});
}
}

这里的 Event 只定义了一个 NoticeId ,其实也可以把请求信息如IP/UA等信息加进去,在 EventHandler里处理以便日后数据分析。

  1. 注册 EventBus 相关服务以及 EventHandlers
services.AddSingleton<IEventBus, EventBus>();
services.AddSingleton<IEventStore, EventStoreInMemory>();
//register EventHandlers
services.AddSingleton<NoticeViewEventHandler>();
  1. 订阅事件
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IEventBus eventBus)
{
eventBus.Subscribe<NoticeViewEvent, NoticeViewEventHandler>();
// ...
}
  1. 发布事件
eventBus.Publish(new NoticeViewEvent { NoticeId = notice.NoticeId });

Reference

动手造轮子:实现一个简单的 EventBus的更多相关文章

  1. 动手造轮子:基于 Redis 实现 EventBus

    动手造轮子:基于 Redis 实现 EventBus Intro 上次我们造了一个简单的基于内存的 EventBus,但是如果要跨系统的话就不合适了,所以有了这篇基于 Redis 的 EventBus ...

  2. 动手造轮子:实现简单的 EventQueue

    动手造轮子:实现简单的 EventQueue Intro 最近项目里有遇到一些并发的问题,想实现一个队列来将并发的请求一个一个串行处理,可以理解为使用消息队列处理并发问题,之前实现过一个简单的 Eve ...

  3. 动手造轮子:实现一个简单的 AOP 框架

    动手造轮子:实现一个简单的 AOP 框架 Intro 最近实现了一个 AOP 框架 -- FluentAspects,API 基本稳定了,写篇文章分享一下这个 AOP 框架的设计. 整体设计 概览 I ...

  4. 开源造轮子:一个简洁,高效,轻量级,酷炫的不要不要的canvas粒子运动插件库

    一:开篇 哈哈哈,感谢标题党的莅临~ 虽然标题有点夸张的感觉,但实际上,插件库确实是简洁,高效,轻量级,酷炫酷炫的咯.废话不多说,先来看个标配例子吧: (codepen在线演示编辑:http://co ...

  5. 重复造轮子,编写一个轻量级的异步写日志的实用工具类(LogAsyncWriter)

    一说到写日志,大家可能推荐一堆的开源日志框架,如:Log4Net.NLog,这些日志框架确实也不错,比较强大也比较灵活,但也正因为又强大又灵活,导致我们使用他们时需要引用一些DLL,同时还要学习各种用 ...

  6. 一起学习造轮子(二):从零开始写一个Redux

    本文是一起学习造轮子系列的第二篇,本篇我们将从零开始写一个小巧完整的Redux,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Promises/A+,Red ...

  7. 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise

    本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...

  8. 一起学习造轮子(三):从零开始写一个React-Redux

    本文是一起学习造轮子系列的第三篇,本篇我们将从零开始写一个React-Redux,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Promises/A+,Re ...

  9. 自己动手实现一个简单的JSON解析器

    1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...

随机推荐

  1. 改善C#程序的建议6:在线程同步中使用信号量

    原文:改善C#程序的建议6:在线程同步中使用信号量 所谓线程同步,就是多个线程之间在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定.C#中对象的类型分为引用类型和值类型.CLR在这两 ...

  2. TStringGrid多选的复制与拷贝

    uses Clipbrd; function StringGridSelectText(mStringGrid: TStringGrid): string; var   I, J: Integer; ...

  3. 知识的内化:学习、实践、输出(与Focus Feedback FixIt的原理是一致的)

    一个人的能力分三个层次: 资源,比如知识.技能.经验.时间.精力.金钱.人脉等 应用流程,即使用资源解决问题的能力,包括做事的方法.流程.策略等,它是你整合应用资源创造价值的能力. 价值取向,即你觉得 ...

  4. java关键字-final

    final特点: 1:这个关键字是一个修饰符,可以修饰类,方法,变量. 2:被final修饰的类是一个最终类,不可以被继承. 3:被final修饰的方法是一个最终方法,不可以被覆盖. 4:被final ...

  5. abp(net core)+easyui+efcore仓储系统——展现层实现增删改查之控制器(六)

    abp(net core)+easyui+efcore仓储系统目录 abp(net core)+easyui+efcore仓储系统——ABP总体介绍(一) abp(net core)+easyui+e ...

  6. AcWing 164. 可达性统计

    给定一张N个点M条边的有向无环图,分别统计从每个点出发能够到达的点的数量. 输入格式 第一行两个整数N,M,接下来M行每行两个整数x,y,表示从x到y的一条有向边. 输出格式 输出共N行,表示每个点能 ...

  7. 基于python语言的自动化测试中生成html的测试报告时HtmlTestRunner模块常见问题

    一.导入了HTMLTestRunner模块,报错:No module named StringIO,在python3.x中确实没有,在第94行引入的名称改成import io,539行要改成self. ...

  8. Windows下OSGEarth的编译过程

    目录 1. 依赖 1) OpenSceneGraph 2) GDAL 3) CURL 4) GEOS 5) 其他 2. 编译 1) 设置参数 2) 配置路径 3) 生成编译 3. 参考文献 1. 依赖 ...

  9. HBase 学习之路(九)——HBase容灾与备份

    一.前言 本文主要介绍Hbase常用的三种简单的容灾备份方案,即CopyTable.Export/Import.Snapshot.分别介绍如下: 二.CopyTable 2.1 简介 CopyTabl ...

  10. 系统学习 Java IO (十一)----打印流 PrintStream

    目录:系统学习 Java IO---- 目录,概览 PrintStream 类可以将格式化数据写入底层 OutputStream 或者直接写入 File 对象. PrintStream 类可以格式化基 ...