简易内存队列,用于合并消息批量处理
目前使用的场景:
1.运营大量发券,推送app消息提醒,由于消费端是多线程BasicAck消费模式,因此大量消息积压
2.数据变更记录,记录数据库所有记录的变化历史,推送到es系统
此类场景的特点几乎所有的消息处理逻辑一样,如app消息需要把手机号转换成推送标识,数据变更需要根据配置过滤部分信息,这些操作通过合并可以减少大量的时间消耗
因此写了一个简易的内存缓冲区用于合并处理
首先核心类MemoryQueue,是一个泛型抽象类,实现了队列、循环消费,开始、结束等功能,继承者需要实现实际的执行方法ExecuteQueue(List<T> list)
如果存在失败的情况,还需要处理ExecuteFailed事件,此事件的第二个参数是自定义EventArgs,包含两个基础属性Exception 和 List<T>
如果消息较为重要,还需要实现自己的IMessageRecord<T>接口,此接口需要有三个方法 Push Remove和GetAll,用于异常结束后队列重新启动继续消费未消费的消息
继承者可以控制队列的最大长度、消费间隔、队列满了重新尝试入队的时间间隔、每次处理的数量
public abstract class MemoryQueue<T> { /// <summary> /// 队列 /// </summary> private readonly ConcurrentQueue<KeyValuePair<string, T>> _queue = new ConcurrentQueue<KeyValuePair<string, T>>(); /// <summary> /// 允许入队 /// </summary> private bool _pushToQueueAllowed = true; /// <summary> /// 循环执行间隔 /// </summary> protected TimeSpan ExecuteInterval = TimeSpan.FromMilliseconds(500); /// <summary> /// 每次处理数量 /// </summary> protected int EveryTimeExecuteCount = 1000; /// <summary> /// 最大内存队列大小 /// </summary> protected int QueueMaxSize = 100000; /// <summary> /// 禁止入队时尝试重新入队的时间间隔 /// </summary> protected TimeSpan EnqueueWaitInterval = TimeSpan.FromMilliseconds(500); private CancellationTokenSource _cancellationTokenSource; Thread _workerThread; /// <summary> /// 处理失败的事件 /// </summary> protected event EventHandler ExecuteFailed; /// <summary> /// 提供简易持久化功能,将消息不做验证迅速存入文件/redis等非易失介质 /// </summary> protected IMessageRecord<T> MessageRecord { get; set; } /// <summary> /// 开始执行 /// </summary> protected virtual void Execute() { while (!_cancellationTokenSource.IsCancellationRequested) { var length = _queue.Count > EveryTimeExecuteCount ? EveryTimeExecuteCount : _queue.Count; var list = new List<T>(length); var msgIds = new List<string>(length); for (int i = 0; i < length; i++) { if (_queue.TryDequeue(out var result)) { list.Add(result.Value); msgIds.Add(result.Key); } else { break; } } if (list.Count > 0) { try { ExecuteQueue(list); } catch (Exception e) { //外部选择异常时是记录参数继续,还是抛出异常停止 OnExecuteFailed(e, list); } finally { //由于异常情况已经将list传递到事件中,有外界决定如何处理 //所以从简易持久化中删除不用区分成功失败 MessageRecord?.Remove(msgIds); } } Thread.Sleep(ExecuteInterval); } } /// <summary> /// 禁止插入队列,关闭时调用 /// </summary> protected virtual void NotAllowedPushToQueue() { _pushToQueueAllowed = false; } /// <summary> /// 当执行失败时触发 /// </summary> /// <param name="e"></param> /// <param name="args"></param> protected virtual void OnExecuteFailed(Exception e, List<T> args) { this.ExecuteFailed?.Invoke((object)this, new ExecuteFailedEventArgs<T>(e, args)); } /// <summary> /// 消费内存队列中的数据 /// </summary> /// <param name="list"></param> protected abstract void ExecuteQueue(List<T> list); /// <summary> /// 开始 /// </summary> /// <returns></returns> public virtual MemoryQueue<T> Start() { if (_cancellationTokenSource == null || _cancellationTokenSource.IsCancellationRequested) { _cancellationTokenSource = new CancellationTokenSource(); //启动时将简易持久化的数据重新添加到队列中 var list = MessageRecord?.GetAll(); if (list != null) { foreach (var item in list) { _queue.Enqueue(item); } } _workerThread = new Thread(Execute) { Name = typeof(T).FullName, IsBackground = true }; _workerThread.Start(); } return this; } /// <summary> /// 尝试停止,如果队列中包含未消费完的数据,等待其消费完,如果60秒还未消费完,则强制结束 /// </summary> /// <returns></returns> public virtual MemoryQueue<T> Stop() { NotAllowedPushToQueue(); double waitMilliseconds = 60000; while (!_queue.IsEmpty && waitMilliseconds > 0) { waitMilliseconds = waitMilliseconds - EnqueueWaitInterval.TotalMilliseconds; Thread.Sleep(EnqueueWaitInterval); } Thread.Sleep(EnqueueWaitInterval); _cancellationTokenSource?.Cancel(); _workerThread?.Abort(); return this; } /// <summary> /// 入队 /// </summary> /// <param name="val">处理的数据</param> /// <param name="msgId">消息Id,不传时会默认使用guid</param> public virtual void PushToQueue(T val, string msgId = null) { if (_cancellationTokenSource == null) { this.Start(); } bool finish = false; while (!finish) { if (_pushToQueueAllowed && _queue.Count < QueueMaxSize) { msgId = msgId ?? Guid.NewGuid().ToString(); //简单持久化 MessageRecord?.Push(msgId, val); _queue.Enqueue(new KeyValuePair<string, T>(msgId, val)); finish = true; } else { Thread.Sleep(EnqueueWaitInterval); } } } }
使用方式:
同一类型的消息公用一个MemoryQueueTest对象,消费时调用MemoryQueueTest.PushToQueue,关闭程序时调用MemoryQueueTest.Stop
public class MemoryQueueTest : MemoryQueue<MemoryQueueTestMessage> { public MemoryQueueTest() { base.EnqueueWaitInterval = TimeSpan.FromSeconds(1); base.EveryTimeExecuteCount = 1000; base.ExecuteInterval = TimeSpan.FromSeconds(5); base.QueueMaxSize = 10000; base.ExecuteFailed += MemoryQueueTest_ExecuteFailed; ; } private void MemoryQueueTest_ExecuteFailed(object sender, EventArgs e) { var arg = (ExecuteFailedEventArgs<MemoryQueueTestMessage>)e; //此处是发生异常时应如何处理,建议仅仅记录日志,不要抛出异常,arg中包含了发生异常的List<T>和异常 } /// <summary> /// 消费内存队列中的数据 /// </summary> /// <param name="list"></param> protected override void ExecuteQueue(List<MemoryQueueTestMessage> list) { //此处是具体的消息执行操作 } }