不少时候,业务有“在一段时间以后,完成一个工做任务”的需求。数组
例如:滴滴打车订单完成后,若是用户一直不评价,48小时后会将自动评价为5星。数据结构
通常来讲怎么实现这类“48小时后自动评价为5星”需求呢?线程
常见方案:启动一个cron定时任务,每小时跑一次,将完成时间超过48小时的订单取出,置为5星,并把评价状态置为已评价。设计
假设订单表的结构为:t_order(oid, finish_time, stars, status, …),更具体的,定时任务每隔一个小时会这么作一次:3d
select oid from t_order where finish_time > 48hours and status=0;指针
update t_order set stars=5 and status=1 where oid in[…];blog
若是数据量很大,须要分页查询,分页update,这将会是一个for循环。队列
方案的不足:ci
(1)轮询效率比较低消息队列
(2)每次扫库,已经被执行过记录,仍然会被扫描(只是不会出如今结果集中),有重复计算的嫌疑
(3)时效性不够好,若是每小时轮询一次,最差的状况下,时间偏差会达到1小时
(4)若是经过增长cron轮询频率来减小(3)中的时间偏差,(1)中轮询低效和(2)中重复计算的问题会进一步凸显
如何利用“延时消息”,对于每一个任务只触发一次,保证效率的同时保证明时性,是今天要讨论的问题。
2、高效延时消息设计与实现
高效延时消息,包含两个重要的数据结构:
(1)环形队列,例如能够建立一个包含3600个slot的环形队列(本质是个数组)
(2)任务集合,环上每个slot是一个Set<Task>
同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。
Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
(2)Task-Function:须要执行的任务指针
假设当前Current Index指向第一格,当有延时消息到达以后,例如但愿3610秒以后,触发一个延时消息任务,只需:
(1)计算这个Task应该放在哪个slot,如今指向1,3610秒以后,应该是第11格,因此这个Task应该放在第11个slot的Set<Task>中
(2)计算这个Task的Cycle-Num,因为环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,因此应该绕3610/3600=1圈以后再执行,因而Cycle-Num=1
Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set<Task>,每一个Task看Cycle-Num是否是0:
(1)若是不是0,说明还须要多移动几圈,将Cycle-Num减1
(2)若是是0,说明立刻要执行这个Task了,取出Task-Funciton执行(能够用单独的线程来执行Task),并把这个Task从Set<Task>中删除
使用了“延时消息”方案以后,“订单48小时后关闭评价”的需求,只需将在订单关闭时,触发一个48小时以后的延时消息便可:
(1)无需再轮询所有订单,效率高
(2)一个订单,任务只执行一次
(3)时效性好,精确到秒(控制timer移动频率能够控制精度)
3、总结
环形队列是一个实现“延时消息”的好方法,开源的MQ好像都不支持延迟消息,不妨本身实现一个简易的“延时消息队列”,能解决不少业务问题,并减小不少低效扫库的cron任务。
另外,关于MQ的可达性、幂等性将来撰文另述。