动手造轮子:实现简单的 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:

public abstract class EventBase
{
[JsonProperty]
public DateTimeOffset EventAt { get; private set; } [JsonProperty]
public string EventId { get; private set; } protected EventBase()
{
this.EventId = GuidIdGenerator.Instance.NewId();
this.EventAt = DateTimeOffset.UtcNow;
} [JsonConstructor]
public EventBase(string eventId, DateTimeOffset eventAt)
{
this.EventId = eventId;
this.EventAt = eventAt;
}
}

EventHandler 定义:

public interface IEventHandler
{
Task Handle(IEventBase @event);
} public interface IEventHandler<in TEvent> : IEventHandler where TEvent : IEventBase
{
Task Handle(TEvent @event);
} public class EventHandlerBase<TEvent> : IEventHandler<TEvent> where TEvent : EventBase
{
public virtual Task Handle(TEvent @event)
{
return Task.CompletedTask;
} public Task Handle(IEventBase @event)
{
return Handle(@event as TEvent);
}
}

EventStore:

public class EventStore
{
private readonly Dictionary<Type, Type> _eventHandlers = new Dictionary<Type, Type>(); public void Add<TEvent, TEventHandler>() where TEventHandler : IEventHandler<TEvent> where TEvent : EventBase
{
_eventHandlers.Add(typeof(TEvent), typeof(TEventHandler));
} public object GetEventHandler(Type eventType, IServiceProvider serviceProvider)
{
if (eventType == null || !_eventHandlers.TryGetValue(eventType, out var handlerType) || handlerType == null)
{
return null;
}
return serviceProvider.GetService(handlerType);
} public object GetEventHandler(EventBase eventBase, IServiceProvider serviceProvider) =>
GetEventHandler(eventBase.GetType(), serviceProvider); public object GetEventHandler<TEvent>(IServiceProvider serviceProvider) where TEvent : EventBase =>
GetEventHandler(typeof(TEvent), serviceProvider);
}

EventQueue 定义:

public class EventQueue
{
private readonly ConcurrentDictionary<string, ConcurrentQueue<EventBase>> _eventQueues =
new ConcurrentDictionary<string, ConcurrentQueue<EventBase>>(); public ICollection<string> Queues => _eventQueues.Keys; public void Enqueue<TEvent>(string queueName, TEvent @event) where TEvent : EventBase
{
var queue = _eventQueues.GetOrAdd(queueName, q => new ConcurrentQueue<EventBase>());
queue.Enqueue(@event);
} public bool TryDequeue(string queueName, out EventBase @event)
{
var queue = _eventQueues.GetOrAdd(queueName, q => new ConcurrentQueue<EventBase>());
return queue.TryDequeue(out @event);
} public bool TryRemoveQueue(string queueName)
{
return _eventQueues.TryRemove(queueName, out _);
} public bool ContainsQueue(string queueName) => _eventQueues.ContainsKey(queueName); public ConcurrentQueue<EventBase> this[string queueName] => _eventQueues[queueName];
}

EventPublisher:

public interface IEventPublisher
{
Task Publish<TEvent>(string queueName, TEvent @event)
where TEvent : EventBase;
}
public class EventPublisher : IEventPublisher
{
private readonly EventQueue _eventQueue; public EventPublisher(EventQueue eventQueue)
{
_eventQueue = eventQueue;
} public Task Publish<TEvent>(string queueName, TEvent @event)
where TEvent : EventBase
{
_eventQueue.Enqueue(queueName, @event);
return Task.CompletedTask;
}
}

EventSubscriptionManager:

public interface IEventSubscriptionManager
{
void Subscribe<TEvent, TEventHandler>()
where TEvent : EventBase
where TEventHandler : IEventHandler<TEvent>;
} public class EventSubscriptionManager : IEventSubscriptionManager
{
private readonly EventStore _eventStore; public EventSubscriptionManager(EventStore eventStore)
{
_eventStore = eventStore;
} public void Subscribe<TEvent, TEventHandler>()
where TEvent : EventBase
where TEventHandler : IEventHandler<TEvent>
{
_eventStore.Add<TEvent, TEventHandler>();
}
}

EventConsumer:

public class EventConsumer : BackgroundService
{
private readonly EventQueue _eventQueue;
private readonly EventStore _eventStore;
private readonly int maxSemaphoreCount = 256;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger; public EventConsumer(EventQueue eventQueue, EventStore eventStore, IConfiguration configuration, ILogger<EventConsumer> logger, IServiceProvider serviceProvider)
{
_eventQueue = eventQueue;
_eventStore = eventStore;
_logger = logger;
_serviceProvider = serviceProvider;
} protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using (var semaphore = new SemaphoreSlim(Environment.ProcessorCount, maxSemaphoreCount))
{
while (!stoppingToken.IsCancellationRequested)
{
var queues = _eventQueue.Queues;
if (queues.Count > 0)
{
await Task.WhenAll(
queues
.Select(async queueName =>
{
if (!_eventQueue.ContainsQueue(queueName))
{
return;
}
try
{
await semaphore.WaitAsync(stoppingToken);
//
if (_eventQueue.TryDequeue(queueName, out var @event))
{
var eventHandler = _eventStore.GetEventHandler(@event, _serviceProvider);
if (eventHandler is IEventHandler handler)
{
_logger.LogInformation(
"handler {handlerType} begin to handle event {eventType}, eventId: {eventId}, eventInfo: {eventInfo}",
eventHandler.GetType().FullName, @event.GetType().FullName,
@event.EventId, JsonConvert.SerializeObject(@event)); try
{
await handler.Handle(@event);
}
catch (Exception e)
{
_logger.LogError(e, "event {eventId} handled exception", @event.EventId);
}
finally
{
_logger.LogInformation("event {eventId} handled", @event.EventId);
}
}
else
{
_logger.LogWarning(
"no event handler registered for event {eventType}, eventId: {eventId}, eventInfo: {eventInfo}",
@event.GetType().FullName, @event.EventId,
JsonConvert.SerializeObject(@event));
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "error running EventConsumer");
}
finally
{
semaphore.Release();
}
})
);
} await Task.Delay(50, stoppingToken);
}
}
}
}

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

public static IServiceCollection AddEvent(this IServiceCollection services)
{
services.TryAddSingleton<EventStore>();
services.TryAddSingleton<EventQueue>();
services.TryAddSingleton<IEventPublisher, EventPublisher>();
services.TryAddSingleton<IEventSubscriptionManager, EventSubscriptionManager>(); services.AddSingleton<IHostedService, EventConsumer>();
return services;
}

使用示例

定义 PageViewEvent 记录请求信息:

public class PageViewEvent : EventBase
{
public string Path { get; set; }
}

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

定义 PageViewEventHandler,处理 PageViewEvent

public class PageViewEventHandler : EventHandlerBase<PageViewEvent>
{
private readonly ILogger _logger; public PageViewEventHandler(ILogger<PageViewEventHandler> logger)
{
_logger = logger;
} public override Task Handle(PageViewEvent @event)
{
_logger.LogInformation($"handle pageViewEvent: {JsonConvert.SerializeObject(@event)}");
return Task.CompletedTask;
}
}

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

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

// 发布 Event 的中间件
app.Use(async (context, next) =>
{
var eventPublisher = context.RequestServices.GetRequiredService<IEventPublisher>();
await eventPublisher.Publish("pageView", new PageViewEvent() { Path = context.Request.Path.Value });
await next();
});

Startup 配置:

public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddEvent();
services.AddSingleton<PageViewEventHandler>();// 注册 Handler
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IEventSubscriptionManager eventSubscriptionManager)
{
eventSubscriptionManager.Subscribe<PageViewEvent, PageViewEventHandler>();
app.Use(async (context, next) =>
{
var eventPublisher = context.RequestServices.GetRequiredService<IEventPublisher>();
await eventPublisher.Publish("pageView", new PageViewEvent() { Path = context.Request.Path.Value });
await next();
});
// ...
}

使用效果:

More

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

  1. Consumer 消息逻辑,现在的实现有些问题,我们的应用场景目前比较简单还可以满足,如果事件比较多就会而且每个事件可能处理需要的时间长短不一样,会导致在一个批次中执行的 Event 中已经完成的事件要等待其他还没完成的事件完成之后才能继续取下一个事件,理想的消费模式应该是各个队列相互独立,在同一个队列中保持顺序消费即可
  2. 上面示例的 EventStore 的实现只是简单的实现了一个事件一个 Handler 的处理情况,实际业务场景中很可能会有一个事件需要多个 Handler 的情况
  3. 这个实现是基于内存的,如果要在分布式场景下使用就不适用了,需要自己实现一下基于redis或者数据库的以满足分布式的需求
  4. and more...

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

Reference

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

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

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

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

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

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

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

  4. h5engine造轮子

    基于学习的造轮子,这是一个最简单,最基础的一个canvas渲染引擎,通过这个引擎架构,可以很快的学习canvas渲染模式! 地址:https://github.com/RichLiu1023/h5en ...

  5. 重新造轮子之静态链接1(Static linking)

    最近学习计算机病毒学的过程中,又讲到了静态链接的问题,联想到了之前保健哥在信息安全的课堂上向我们展示了一个没有main()函数的C程序到底应该如何编写.个人觉得这个小实验对于加深静态链接的过程的理解也 ...

  6. React造轮子:拖拽排序组件「Dragact」

    先来一张图看看: 项目地址:Github地址 (无耻求星!) 在线观看(第一次加载需要等几秒):预览地址 说起来不容易,人在国外没有过年一说,但是毕竟也是中国年,虽然不放假,但是家里总会主内一顿丰盛的 ...

  7. 造轮子系列之RPC 1:如何从零开始开发RPC框架

    前言 RPC 框架是后端攻城狮永远都绕不开的知识点,目前业界比较知名有 Dubbo.Spring Cloud 等.很多人都停留在了只会用的阶段,作为程序猿,拥有好奇心深入学习,才能有效提高自己的竞争力 ...

  8. 【疯狂造轮子-iOS】JSON转Model系列之二

    [疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...

  9. 【疯狂造轮子-iOS】JSON转Model系列之一

    [疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...

随机推荐

  1. Linux用到的常用命令

    Linux常用命令        

  2. 【python数据分析实战】电影票房数据分析(一)数据采集

    目录 1.获取url 2.开始采集 3.存入mysql 本文是爬虫及可视化的练习项目,目标是爬取猫眼票房的全部数据并做可视化分析. 1.获取url 我们先打开猫眼票房http://piaofang.m ...

  3. Python小工具:利用ffmpy3库3秒钟将视频转换为音频

    作者 | pk 哥 来源公众号 | Python知识圈(ID:PythonCircle) 最近,有读者微信上私聊我,想让我写一篇视频批量转换成音频的文章,我答应了,周末宅家里把这个小工具做出来了. 这 ...

  4. 图像处理 - ImageMagick 简单介绍与案例

    在客户端我们可以用 PhotoShop 等 GUI 工具处理静态图片或者动态 GIF 图片,不过在服务器端对于 WEB 应用程序要处理图片格式转换,缩放裁剪,翻转扭曲,PDF解析等操作, GUI 软件 ...

  5. git jenkins 介绍

    1.什么是持续集成?2.什么是持续交付?3.什么是持续部署?4.版本控制工具? git    1.git基本操作    2.git 分支    3.git 标签tag    4.gitlab  git ...

  6. 使用诊断工具观察 Microsoft.Extensions.DependencyInjection 2.x 版本的内存占用

    目录 准备工作 大量接口与实现类的生成 elasticsearch+kibana+apm asp.net core 应用 请求与快照 Kibana 上的请求记录 请求耗时的分析 请求内存的分析 第2次 ...

  7. Nginx在Window上简单的使用

    先上Nginx在Window上的基本常用指令: IP_hase也可以解决Session共享的问题:不过不推荐这样使用,建议使用 Memcache/redis来处理 session共享的问题 轮询还是权 ...

  8. Java基础(43)Queue队列

    Collection接口的第三个子接口是Queue接口,而Queue接口的子接口又是Deque接口和BlockingQueue接口. 实现了Deque接口的类有:ArrayDeque类.Concurr ...

  9. vue-music 使用better-scroll遇到轮播图不能自动轮播

    根据vue-music视频中slider组建的使用,当安装新版本的better-scroll,轮播组件,不能正常轮播 这是因为,better-scroll发布新版本之后,参数设置发生改变 这是旧版本: ...

  10. SpringBoot与热部署整合(五)

    一 Idea pom.xml <dependency> <groupId>org.springframework.boot</groupId> <artifa ...