一、概要
在工作当中遇到了一个需要定时向客户端推送新闻、文章等内容。这个时候在网上搜了很久没有找到合适的解决方案,其实能解决这个问题的方案有很多比如说用到一些大厂贡献的xxMQ中间件之类的,确实能解决问题。但是目前项目比较小根本用不上这么重的框架,在偶然的看到了一位大佬写的文章提供了一个非常不错的思路本篇文章也是受到他的启发实现了之后这里分享给大家。这个大佬的是58的沈剑文章名称是“1分钟实现延迟消息功能”。
原文地址:
二、详细内容
详细内容大概分为4个部分,1.应用场景 2.遇到问题 3.设计 4.实现 5.运行效果
1.应用场景
需要定时推送数据,且轻量化的实现。
2.遇到问题
3.设计
高效延时消息,包含两个重要的数据结构:
(1)环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)
(2)任务集合,环上每一个slot是一个Set
同时,启动一个t*mer,这个t*mer每隔1s,在上述环形队列中移动一格,有一个Curre*t I*dex指针来标识正在检测的slot。
Task结构中有两个很重要的属性:
(1)Cycle-Num:当Curre*t I*dex第几圈扫描到这个Slot时,执行任务
(2)Task-Fu*ct*o*:需要执行的任务指针
假设当前Curre*t I*dex指向第一格,当有延时消息到达之后,例如希望3610秒之后,触发一个延时消息任务,只需:
(1)计算这个Task应该放在哪一个slot,现在指向1,3610秒之后,应该是第11格,所以这个Task应该放在第11个slot的Set中
(2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1
Curre*t I*dex不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0:
(1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1
(2)如果是0,说明马上要执行这个Task了,取出Task-Fu*c*to*执行(可以用单独的线程来执行Task),并把这个Task从Set中删除
使用了“延时消息”方案之后,“订单48小时后关闭评价”的需求,只需将在订单关闭时,触发一个48小时之后的延时消息即可:
(1)无需再轮询全部订单,效率高
(2)一个订单,任务只执行一次
(3)时效性好,精确到秒(控制t*mer移动频率可以控制精度)
4.实现
首先写一个方案要理清楚自己的项目结构,我做了如下分层。
&*bsp;
<*m* src="https://p*c3.zh*m*.com/80/v2-aa161aef9597768a6e4194b5f1fe461e_720w.jp*" w*dth="255" class="co*te*t_*ma*e lazy" data-capt*o*="" data-s*ze="*ormal" data-raww*dth="255" data-rawhe**ht="332" data-actualsrc="https://p*c3.zh*m*.com/v2-aa161aef9597768a6e4194b5f1fe461e_b.jp*" data-lazy-status="ok" />
&*bsp;
I*terfaces , 这层里主要约束延迟消息队列的队列和消息任务行。
publ*c **terface IR***Queue<T&*t;
{
/// <summary&*t;
/// Add tasks [add tasks w*ll automat*cally *e*erate: task Id, task slot locat*o*, *umber of task cycles]
/// </summary&*t;
/// <param *ame="delayT*me"&*t;The spec*f*ed task *s executed after N seco*ds.</param&*t;
/// <param *ame="act*o*"&*t;Def***t*o*s of callback</param&*t;
vo*d Add(lo** delayT*me,Act*o*<T&*t; act*o*);
/// <summary&*t;
/// Add tasks [add tasks w*ll automat*cally *e*erate: task Id, task slot locat*o*, *umber of task cycles]
/// </summary&*t;
/// <param *ame="delayT*me"&*t;The spec*f*ed task *s executed after N seco*ds.</param&*t;
/// <param *ame="act*o*"&*t;Def***t*o*s of callback.</param&*t;
/// <param *ame="data"&*t;*arameters used ** the callback fu*ct*o*.</param&*t;
vo*d Add(lo** delayT*me, Act*o*<T&*t; act*o*, T data);
/// <summary&*t;
/// Add tasks [add tasks w*ll automat*cally *e*erate: task Id, task slot locat*o*, *umber of task cycles]
/// </summary&*t;
/// <param *ame="delayT*me"&*t;</param&*t;
/// <param *ame="act*o*"&*t;Def***t*o*s of callback</param&*t;
/// <param *ame="data"&*t;*arameters used ** the callback fu*ct*o*.</param&*t;
/// <param *ame="*d"&*t;Task ID, used whe* delet*** tasks.</param&*t;
vo*d Add(lo** delayT*me, Act*o*<T&*t; act*o*, T data, lo** *d);
/// <summary&*t;
/// Remove tasks [*eed to k*ow: where the task *s, wh*ch spec*f*c task].
/// </summary&*t;
/// <param *ame="**dex"&*t;Task slot locat*o*</param&*t;
/// <param *ame="*d"&*t;Task ID, used whe* delet*** tasks.</param&*t;
vo*d Remove(lo** *d);
/// <summary&*t;
/// Lau*ch queue.
/// </summary&*t;
vo*d Start();
}
publ*c **terface ITask
{
}
Ach*eves,这层里实现之前定义的接口,这里写成抽象类是为了后面方便扩展。
us*** System;
us*** System.Collect*o*s.Co*curre*t;
us*** System.L**q;
us*** System.Thread***;
us*** System.Thread***.Tasks;
us*** DelayMessa*eApp.I*terfaces;
*amespace DelayMessa*eApp.Ach*eves.Base
{
publ*c abstract class BaseQueue<T&*t; : IR***Queue<T&*t;
{
pr*vate lo** _po**ter = 0L;
pr*vate Co*curre*tBa*<BaseTask<T&*t;&*t;[] _arraySlot;
pr*vate **t ArrayMax;
/// <summary&*t;
/// R*** queue.
/// </summary&*t;
publ*c Co*curre*tBa*<BaseTask<T&*t;&*t;[] ArraySlot
{
*et { retur* _arraySlot ?? (_arraySlot = *ew Co*curre*tBa*<BaseTask<T&*t;&*t;[ArrayMax]); }
}
publ*c BaseQueue(**t arrayMax)
{
*f (arrayMax < 60 && arrayMax % 60 == 0)
throw *ew Except*o*("R*** queue le**th ca**ot be less tha* 60 a*d *s a mult*ple of 60 .");
ArrayMax = arrayMax;
}
publ*c vo*d Add(lo** delayT*me, Act*o*<T&*t; act*o*)
{
Add(delayT*me, act*o*, default(T));
}
publ*c vo*d Add(lo** delayT*me,Act*o*<T&*t; act*o*,T data)
{
Add(delayT*me, act*o*, data,0);
}
publ*c vo*d Add(lo** delayT*me, Act*o*<T&*t; act*o*, T data,lo** *d)
{
NextSlot(delayT*me, out lo** cycle, out lo** po**ter);
ArraySlot[po**ter] = ArraySlot[po**ter] ?? (ArraySlot[po**ter] = *ew Co*curre*tBa*<BaseTask<T&*t;&*t;());
var baseTask = *ew BaseTask<T&*t;(cycle, act*o*, data,*d);
ArraySlot[po**ter].Add(baseTask);
}
/// <summary&*t;
/// Remove tasks based o* ID.
/// </summary&*t;
/// <param *ame="*d"&*t;</param&*t;
publ*c vo*d Remove(lo** *d)
{
try
{
*arallel.ForEach(ArraySlot, (Co*curre*tBa*<BaseTask<T&*t;&*t; collect*o*, *arallelLoopState state) =&*t;
{
var resulTask = collect*o*.F*rstOrDefault(p =&*t; p.Id == *d);
*f (resulTask != *ull)
{
collect*o*.TryTake(out resulTask);
state.Break();
}
});
}
catch (Except*o* e)
{
Co*sole.Wr*teL**e(e);
}
}
publ*c vo*d Start()
{
wh*le (true)
{
R**htMove*o**ter();
Thread.Sleep(1000);
Co*sole.Wr*teL**e(DateT*me.Now.ToStr***());
}
}
/// <summary&*t;
/// Calculate the **format*o* of the *ext slot.
/// </summary&*t;
/// <param *ame="delayT*me"&*t;Delayed execut*o* t*me.</param&*t;
/// <param *ame="cycle"&*t;Number of tur*s.</param&*t;
/// <param *ame="**dex"&*t;Task locat*o*.</param&*t;
pr*vate vo*d NextSlot(lo** delayT*me, out lo** cycle,out lo** **dex)
{
try
{
var c*rcle = delayT*me / ArrayMax;
var seco*d = delayT*me % ArrayMax;
var curre*t_po**ter = Get*o**ter();
var queue_**dex = 0L;
*f (delayT*me - ArrayMax &*t; ArrayMax)
{
c*rcle = 1;
}
else *f (seco*d &*t; ArrayMax)
{
c*rcle += 1;
}
*f (delayT*me - c*rcle * ArrayMax < ArrayMax)
{
seco*d = delayT*me - c*rcle * ArrayMax;
}
*f (curre*t_po**ter + delayT*me &*t;= ArrayMax)
{
cycle = (**t)((curre*t_po**ter + delayT*me) / ArrayMax);
*f (curre*t_po**ter + seco*d - ArrayMax < 0)
{
queue_**dex = curre*t_po**ter + seco*d;
}
else *f (curre*t_po**ter + seco*d - ArrayMax &*t; 0)
{
queue_**dex = curre*t_po**ter + seco*d - ArrayMax;
}
}
else
{
cycle = 0;
queue_**dex = curre*t_po**ter + seco*d;
}
**dex = queue_**dex;
}
catch (Except*o* e)
{
Co*sole.Wr*teL**e(e);
throw;
}
}
/// <summary&*t;
/// Get the curre*t locat*o* of the po**ter.
/// </summary&*t;
/// <retur*s&*t;</retur*s&*t;
pr*vate lo** Get*o**ter()
{
retur* I*terlocked.Read(ref _po**ter);
}
/// <summary&*t;
/// Reset po**ter pos*t*o*.
/// </summary&*t;
pr*vate vo*d ReSet*o**ter()
{
I*terlocked.Excha**e(ref _po**ter, 0);
}
/// <summary&*t;
/// *o**ter moves clockw*se.
/// </summary&*t;
pr*vate vo*d R**htMove*o**ter()
{
try
{
*f (Get*o**ter() &*t;= ArrayMax - 1)
{
ReSet*o**ter();
}
else
{
I*terlocked.I*creme*t(ref _po**ter);
}
var po**ter = Get*o**ter();
var taskCollect*o* = ArraySlot[po**ter];
*f (taskCollect*o* == *ull || taskCollect*o*.Cou*t == 0) retur*;
*arallel.ForEach(taskCollect*o*, (BaseTask<T&*t; task) =&*t;
{
*f (task.Cycle &*t; 0)
{
task.SubCycleNumber();
}
*f (task.Cycle <= 0)
{
taskCollect*o*.TryTake(out task);
task.TaskAct*o*(task.Data);
}
});
}
catch (Except*o* e)
{
Co*sole.Wr*teL**e(e);
throw;
}
}
}
}
us*** System;
us*** System.Thread***;
us*** DelayMessa*eApp.I*terfaces;
*amespace DelayMessa*eApp.Ach*eves.Base
{
publ*c class BaseTask<T&*t; : ITask
{
pr*vate lo** _cycle;
pr*vate lo** _*d;
pr*vate T _data;
publ*c Act*o*<T&*t; TaskAct*o* { *et; set; }
publ*c lo** Cycle
{
*et { retur* I*terlocked.Read(ref _cycle); }
set { I*terlocked.Excha**e(ref _cycle, value); }
}
publ*c lo** Id
{
*et { retur* _*d; }
set { _*d = value; }
}
publ*c T Data
{
*et { retur* _data; }
set { _data = value; }
}
publ*c BaseTask(lo** cycle, Act*o*<T&*t; act*o*, T data,lo** *d)
{
Cycle = cycle;
TaskAct*o* = act*o*;
Data = data;
Id = *d;
}
publ*c BaseTask(lo** cycle, Act*o*<T&*t; act*o*,T data)
{
Cycle = cycle;
TaskAct*o* = act*o*;
Data = data;
}
publ*c BaseTask(lo** cycle, Act*o*<T&*t; act*o*)
{
Cycle = cycle;
TaskAct*o* = act*o*;
}
publ*c vo*d SubCycleNumber()
{
I*terlocked.Decreme*t(ref _cycle);
}
}
}
Lo**c,这层主要实现调用逻辑,调用者最终只需要关心把任务放进队列并指定什么时候执行就行了,根本不需要关心其它的任何信息。
publ*c stat*c vo*d Start()
{
//1.I**t*al*ze queues of d*ffere*t *ra*ular*ty.
IR***Queue<NewsModel&*t; m**uteR***Queue = *ew M**uteQueue<NewsModel&*t;();
//2.Ope* thread.
var lstTasks = *ew L*st<Task&*t;
{
Task.Factory.StartNew(m**uteR***Queue.Start)
};
//3.Add tasks performed ** d*ffere*t per*ods.
m**uteR***Queue.Add(5, *ew Act*o*<NewsModel&*t;((NewsModel *ewsObj) =&*t;
{
Co*sole.Wr*teL**e(*ewsObj.News);
}), *ew NewsModel() { News = "Trump's v*s*t to Ch**a!" });
m**uteR***Queue.Add(10, *ew Act*o*<NewsModel&*t;((NewsModel *ewsObj) =&*t;
{
Co*sole.Wr*teL**e(*ewsObj.News);
}), *ew NewsModel() { News = "*ut** *u's v*s*t to Ch**a!" });
m**uteR***Queue.Add(60, *ew Act*o*<NewsModel&*t;((NewsModel *ewsObj) =&*t;
{
Co*sole.Wr*teL**e(*ewsObj.News);
}), *ew NewsModel() { News = "E*se*hower's v*s*t to Ch**a!" });
m**uteR***Queue.Add(120, *ew Act*o*<NewsModel&*t;((NewsModel *ewsObj) =&*t;
{
Co*sole.Wr*teL**e(*ewsObj.News);
}), *ew NewsModel() { News = "** ***p***'s v*s*t to the US!" });
//3.Wa*t*** for all tasks to complete *s usually *ot completed. Because there *s a* **f***te loop.
//F5 Ru* the pro*ram a*d see the effect.
Task.Wa*tAll(lstTasks.ToArray());
Co*sole.Read();
}
Models,这层就是用来在延迟任务中带入的数据模型类而已了。自己用的时候换成任意自定义类型都可以。
5.运行效果
<*m* src="https://p*c4.zh*m*.com/80/v2-6d531793db60690eeca9c358bf41bd77_720w.jp*" w*dth="374" class="co*te*t_*ma*e lazy" data-capt*o*="" data-s*ze="*ormal" data-raww*dth="374" data-rawhe**ht="263" data-actualsrc="https://p*c4.zh*m*.com/v2-6d531793db60690eeca9c358bf41bd77_b.jp*" data-lazy-status="ok" />