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

Intro

最近项目里有遇到一些并发的问题,想实现一个队列来将并发的请求一个一个串行处理,可以理解为使用消息队列处理并发问题,之前实现过一个简单的 EventBus,于是想在 EventBus 的基础上改造一下,加一个队列,改造成类似消息队列的处理模式。消息的处理(Consumer)直接使用 .netcore 里的 IHostedService 来实现了一个简单的后台任务处理。

初步设计

  • Event 抽象的事件

  • EventHandler 处理 Event 的方法

  • EventStore 保存订阅 Event 的 EventHandler

  • EventQueue 保存 Event 的队列

  • EventPublisher 发布 Event

  • EventConsumer 处理 Event 队列里的 Event

  • EventSubscriptionManager 管理订阅 Event 的 EventHandler

实现代码

EventBase 定义了基本事件信息,事件发生时间以及事件的id:

  1. public abstract class EventBase

  2. {

  3. [JsonProperty]

  4. public DateTimeOffset EventAt { get; private set; }

  5. [JsonProperty]

  6. public string EventId { get; private set; }

  7. protected EventBase()

  8. {

  9. this.EventId = GuidIdGenerator.Instance.NewId();

  10. this.EventAt = DateTimeOffset.UtcNow;

  11. }

  12. [JsonConstructor]

  13. public EventBase(string eventId, DateTimeOffset eventAt)

  14. {

  15. this.EventId = eventId;

  16. this.EventAt = eventAt;

  17. }

  18. }

EventHandler 定义:

  1. public interface IEventHandler

  2. {

  3. Task Handle(IEventBase @event);

  4. }

  5. public interface IEventHandler<in TEvent> : IEventHandler where TEvent : IEventBase

  6. {

  7. Task Handle(TEvent @event);

  8. }

  9. public class EventHandlerBase<TEvent> : IEventHandler<TEvent> where TEvent : EventBase

  10. {

  11. public virtual Task Handle(TEvent @event)

  12. {

  13. return Task.CompletedTask;

  14. }

  15. public Task Handle(IEventBase @event)

  16. {

  17. return Handle(@event as TEvent);

  18. }

  19. }

EventStore:

  1. public class EventStore

  2. {

  3. private readonly Dictionary<Type, Type> _eventHandlers = new Dictionary<Type, Type>();

  4. public void Add<TEvent, TEventHandler>() where TEventHandler : IEventHandler<TEvent> where TEvent : EventBase

  5. {

  6. _eventHandlers.Add(typeof(TEvent), typeof(TEventHandler));

  7. }

  8. public object GetEventHandler(Type eventType, IServiceProvider serviceProvider)

  9. {

  10. if (eventType == null || !_eventHandlers.TryGetValue(eventType, out var handlerType) || handlerType == null)

  11. {

  12. return null;

  13. }

  14. return serviceProvider.GetService(handlerType);

  15. }

  16. public object GetEventHandler(EventBase eventBase, IServiceProvider serviceProvider) =>

  17. GetEventHandler(eventBase.GetType(), serviceProvider);

  18. public object GetEventHandler<TEvent>(IServiceProvider serviceProvider) where TEvent : EventBase =>

  19. GetEventHandler(typeof(TEvent), serviceProvider);

  20. }

EventQueue 定义:

  1. public class EventQueue

  2. {

  3. private readonly ConcurrentDictionary<string, ConcurrentQueue<EventBase>> _eventQueues =

  4. new ConcurrentDictionary<string, ConcurrentQueue<EventBase>>();

  5. public ICollection<string> Queues => _eventQueues.Keys;

  6. public void Enqueue<TEvent>(string queueName, TEvent @event) where TEvent : EventBase

  7. {

  8. var queue = _eventQueues.GetOrAdd(queueName, q => new ConcurrentQueue<EventBase>());

  9. queue.Enqueue(@event);

  10. }

  11. public bool TryDequeue(string queueName, out EventBase @event)

  12. {

  13. var queue = _eventQueues.GetOrAdd(queueName, q => new ConcurrentQueue<EventBase>());

  14. return queue.TryDequeue(out @event);

  15. }

  16. public bool TryRemoveQueue(string queueName)

  17. {

  18. return _eventQueues.TryRemove(queueName, out _);

  19. }

  20. public bool ContainsQueue(string queueName) => _eventQueues.ContainsKey(queueName);

  21. public ConcurrentQueue<EventBase> this[string queueName] => _eventQueues[queueName];

  22. }

EventPublisher:

  1. public interface IEventPublisher

  2. {

  3. Task Publish<TEvent>(string queueName, TEvent @event)

  4. where TEvent : EventBase;

  5. }

  6. public class EventPublisher : IEventPublisher

  7. {

  8. private readonly EventQueue _eventQueue;

  9. public EventPublisher(EventQueue eventQueue)

  10. {

  11. _eventQueue = eventQueue;

  12. }

  13. public Task Publish<TEvent>(string queueName, TEvent @event)

  14. where TEvent : EventBase

  15. {

  16. _eventQueue.Enqueue(queueName, @event);

  17. return Task.CompletedTask;

  18. }

  19. }

EventSubscriptionManager:

  1. public interface IEventSubscriptionManager

  2. {

  3. void Subscribe<TEvent, TEventHandler>()

  4. where TEvent : EventBase

  5. where TEventHandler : IEventHandler<TEvent>;

  6. }

  7. public class EventSubscriptionManager : IEventSubscriptionManager

  8. {

  9. private readonly EventStore _eventStore;

  10. public EventSubscriptionManager(EventStore eventStore)

  11. {

  12. _eventStore = eventStore;

  13. }

  14. public void Subscribe<TEvent, TEventHandler>()

  15. where TEvent : EventBase

  16. where TEventHandler : IEventHandler<TEvent>

  17. {

  18. _eventStore.Add<TEvent, TEventHandler>();

  19. }

  20. }

EventConsumer:

  1. public class EventConsumer : BackgroundService

  2. {

  3. private readonly EventQueue _eventQueue;

  4. private readonly EventStore _eventStore;

  5. private readonly int maxSemaphoreCount = 256;

  6. private readonly IServiceProvider _serviceProvider;

  7. private readonly ILogger _logger;

  8. public EventConsumer(EventQueue eventQueue, EventStore eventStore, IConfiguration configuration, ILogger<EventConsumer> logger, IServiceProvider serviceProvider)

  9. {

  10. _eventQueue = eventQueue;

  11. _eventStore = eventStore;

  12. _logger = logger;

  13. _serviceProvider = serviceProvider;

  14. }

  15. protected override async Task ExecuteAsync(CancellationToken stoppingToken)

  16. {

  17. using (var semaphore = new SemaphoreSlim(Environment.ProcessorCount, maxSemaphoreCount))

  18. {

  19. while (!stoppingToken.IsCancellationRequested)

  20. {

  21. var queues = _eventQueue.Queues;

  22. if (queues.Count > 0)

  23. {

  24. await Task.WhenAll(

  25. queues

  26. .Select(async queueName =>

  27. {

  28. if (!_eventQueue.ContainsQueue(queueName))

  29. {

  30. return;

  31. }

  32. try

  33. {

  34. await semaphore.WaitAsync(stoppingToken);

  35. //

  36. if (_eventQueue.TryDequeue(queueName, out var @event))

  37. {

  38. var eventHandler = _eventStore.GetEventHandler(@event, _serviceProvider);

  39. if (eventHandler is IEventHandler handler)

  40. {

  41. _logger.LogInformation(

  42. "handler {handlerType} begin to handle event {eventType}, eventId: {eventId}, eventInfo: {eventInfo}",

  43. eventHandler.GetType().FullName, @event.GetType().FullName,

  44. @event.EventId, JsonConvert.SerializeObject(@event));

  45. try

  46. {

  47. await handler.Handle(@event);

  48. }

  49. catch (Exception e)

  50. {

  51. _logger.LogError(e, "event {eventId} handled exception", @event.EventId);

  52. }

  53. finally

  54. {

  55. _logger.LogInformation("event {eventId} handled", @event.EventId);

  56. }

  57. }

  58. else

  59. {

  60. _logger.LogWarning(

  61. "no event handler registered for event {eventType}, eventId: {eventId}, eventInfo: {eventInfo}",

  62. @event.GetType().FullName, @event.EventId,

  63. JsonConvert.SerializeObject(@event));

  64. }

  65. }

  66. }

  67. catch (Exception ex)

  68. {

  69. _logger.LogError(ex, "error running EventConsumer");

  70. }

  71. finally

  72. {

  73. semaphore.Release();

  74. }

  75. })

  76. );

  77. }

  78. await Task.Delay(50, stoppingToken);

  79. }

  80. }

  81. }

  82. }

为了方便使用定义了一个 Event 扩展方法:

  1. public static IServiceCollection AddEvent(this IServiceCollection services)

  2. {

  3. services.TryAddSingleton<EventStore>();

  4. services.TryAddSingleton<EventQueue>();

  5. services.TryAddSingleton<IEventPublisher, EventPublisher>();

  6. services.TryAddSingleton<IEventSubscriptionManager, EventSubscriptionManager>();

  7. services.AddSingleton<IHostedService, EventConsumer>();

  8. return services;

  9. }

使用示例

定义 PageViewEvent 记录请求信息:

  1. public class PageViewEvent : EventBase

  2. {

  3. public string Path { get; set; }

  4. }

这里作为示例只记录了请求的Path信息,实际使用可以增加更多需要记录的信息

定义 PageViewEventHandler,处理 PageViewEvent

  1. public class PageViewEventHandler : EventHandlerBase<PageViewEvent>

  2. {

  3. private readonly ILogger _logger;

  4. public PageViewEventHandler(ILogger<PageViewEventHandler> logger)

  5. {

  6. _logger = logger;

  7. }

  8. public override Task Handle(PageViewEvent @event)

  9. {

  10. _logger.LogInformation($"handle pageViewEvent: {JsonConvert.SerializeObject(@event)}");

  11. return Task.CompletedTask;

  12. }

  13. }

这个 handler 里什么都没做只是输出一个日志

这个示例项目定义了一个记录请求路径的事件以及一个发布请求记录事件的中间件

  1. // 发布 Event 的中间件

  2. app.Use(async (context, next) =>

  3. {

  4. var eventPublisher = context.RequestServices.GetRequiredService<IEventPublisher>();

  5. await eventPublisher.Publish("pageView", new PageViewEvent() { Path = context.Request.Path.Value });

  6. await next();

  7. });

Startup 配置:

  1. public void ConfigureServices(IServiceCollection services)

  2. {

  3. // ...

  4. services.AddEvent();

  5. services.AddSingleton<PageViewEventHandler>();// 注册 Handler

  6. }

  7. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

  8. public void Configure(IApplicationBuilder app, IHostingEnvironment env, IEventSubscriptionManager eventSubscriptionManager)

  9. {

  10. eventSubscriptionManager.Subscribe<PageViewEvent, PageViewEventHandler>();

  11. app.Use(async (context, next) =>

  12. {

  13. var eventPublisher = context.RequestServices.GetRequiredService<IEventPublisher>();

  14. await eventPublisher.Publish("pageView", new PageViewEvent() { Path = context.Request.Path.Value });

  15. await next();

  16. });

  17. // ...

  18. }

使用效果:

More

注:只是一个初步设计,基本可以实现功能,还是有些不足,实际应用的话还有一些要考虑的事情

  1. Consumer 消息逻辑,现在的实现有些问题,我们的应用场景目前比较简单还可以满足,如果事件比较多就会而且每个事件可能处理需要的时间长短不一样,会导致在一个批次中执行的 Event 中已经完成的事件要等待其他还没完成的事件完成之后才能继续取下一个事件,理想的消费模式应该是各个队列相互独立,在同一个队列中保持顺序消费即可

  2. 上面示例的 EventStore 的实现只是简单的实现了一个事件一个 Handler 的处理情况,实际业务场景中很可能会有一个事件需要多个 Handler 的情况

  3. 这个实现是基于内存的,如果要在分布式场景下使用就不适用了,需要自己实现一下基于redis或者数据库的以满足分布式的需求

  4. and more...

上面所有的代码可以在 Github 上获取,示例项目 Github 地址:https://github.com/WeihanLi/AspNetCorePlayground/tree/master/TestWebApplication

Reference

  • https://github.com/WeihanLi/AspNetCorePlayground/tree/master/TestWebApplication/Event

动手造轮子:实现简单的 EventQueue相关推荐

  1. 动手造轮子:实现一个简单的基于 Console 的日志输出

    动手造轮子:实现一个简单的基于 Console 的日志输出 Intro 之前结合了微软的 Logging 框架和 Serilog 写了一个简单的日志框架,但是之前的用法都是基于 log4net.ser ...

  2. 动手造轮子:实现一个简单的依赖注入(二) --- 服务注册优化

    动手造轮子:实现一个简单的依赖注入(二) --- 服务注册优化 Intro 之前实现的那版依赖注入框架基本可用,但是感觉还是不够灵活,而且注册服务和解析服务在同一个地方感觉有点别扭,有点职责分离不够. ...

  3. 动手造轮子:实现一个简单的依赖注入(一)

    动手造轮子:实现一个简单的依赖注入(一) Intro 在上一篇文章中主要介绍了一下要做的依赖注入的整体设计和大概编程体验,这篇文章要开始写代码了,开始实现自己的依赖注入框架. 类图 首先来温习一下上次 ...

  4. 动手造轮子:实现一个简单的依赖注入(零)

    动手造轮子:实现一个简单的依赖注入(零) Intro 依赖注入为我们写程序带来了诸多好处,在微软的 .net core 出来的同时也发布了微软开发的依赖注入框架 Microsoft.Extension ...

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

    动手造轮子:实现一个简单的 EventBus Intro EventBus 是一种事件发布订阅模式,通过 EventBus 我们可以很方便的实现解耦,将事件的发起和事件的处理的很好的分隔开来,很好的实 ...

  6. 动手造轮子:写一个日志框架

    动手造轮子:写一个日志框架 Intro 日志框架有很多,比如 log4net / nlog / serilog / microsoft.extensions.logging 等,如何在切换日志框架的时 ...

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

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

  8. python安装轮子_自己动手造“轮子”---python常用的几个方法

    前言:由于工作内容的原因,经常需要些python脚本,久而久之,发现有一些方法经常用到,于是就自己动手编辑了一些常用的.大众的.通用的方法.小弟不才,但也希望能为开源做做贡献. 最后再附上代码哈: 一 ...

  9. 造轮子之图片轮播组件(swiper)

    图片轮播是种很常见的场景和功能,一般移动网站首页的轮播 banner,商品详情页的商品图片等位置都会用到此功能 像这种常用的场景功能肯定是有人早就写好插件了的,所以遇到这种场景,一般都遵循以下三步: ...

最新文章

  1. 标志位和中断位的区别:USART_ClearFlag和USART_ClearITPendingBit
  2. 人类血液中首次发现微塑料颗粒,饮料瓶塑料袋化妆品都是来源
  3. 数据结构例程——线性表顺序存储的应用
  4. 不同级别UI设计师的区别有哪些?
  5. Oracle 存储过程错误之PLS-00201: 必须声明标识符
  6. linux中时间命令详解
  7. centos下openfire安装(转)
  8. 应届生想要获取web前端开发岗位?这份技能攻略,面试攻略别错过
  9. 什么才是一份好的AI求职简历?
  10. 2015年第六届蓝桥杯国赛试题(JavaA组)
  11. IE设置自动获得代理(ISA20042006中设置相应项)
  12. maven指定配置文件
  13. fst和skiplist
  14. win7计算机怎么初始化,Win7系统网络初始化的设置方法
  15. 随机梯度下降法(stochastic gradient descent,SGD)
  16. 腾讯位置大数据中区域热力图数据的数据解析
  17. PyPDF2--如何使用python操作你的PDF文档
  18. C语言 输出Sn = a + aa + aaa + aaaa + ······
  19. 使用python画函数图像
  20. jQuery实现一组图片的循环滚动

热门文章

  1. 持续集成之配置TeamCity
  2. DB2 pureScale在线备份恢复实例1
  3. 交换网络中的sniffer讨论-基于交换网络的ARP spoofing sniffer
  4. 怎样去掉桌面图标和字的蓝色阴影
  5. Silverlight专题(10)- WatermarkedTextBox使用
  6. Unity3D学习笔记之六创建更多的Prefab
  7. 使用dotnet template快速开发Microsoft Teams Outgoing Web Hook
  8. 读名老中医之路笔记(二)
  9. 设置状态栏和标题栏的样式
  10. 感受机房管理化繁为简-新款KVM使用心得