一、引言
1.1 背景
项目开发中,我们经常会遇到这些场景:
- 1.小程序商城用户下单,30分钟订单未付款自动取消。
- 2.秒杀活动开始前15分钟对设置了秒杀提醒的用户进行推送。
- 3.用户成功秒杀到商品,5分钟未提交订单退回商品。
上述场景传统的解决方案一般是采用定时任务执行,通过设置定时任务的执行间隔,定时进行检测执行。
参考:分布式定时任务调度框架实践
1.2 缺陷
然而,定时任务的方案会存在以下不足:
- 1.触发时间和频率不好设置。
- 2.不能满足实时性要求高的场景。
- 3.会导致任务对应业务处理空跑的情况,耗费资源。大部分定时任务查询不到任何数据。对宝贵的数据库资源产生不必要的消耗。
针对如上业务特点,更好的处理方式是使用延时任务技术方案,目前主流的延时任务实现方式有:
- Kafka延时消息
- Netty时间轮算法
二、Kafka延时消息的时间轮算法
tick: 时间轮里每一格;
tickDuration: 每一格的时长;
ticksPerWheel: 时间轮总共有多少格.(图中8格)
newTimeout: 定时任务分配到时间轮
时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList)。
TimerTaskList任务列表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务TimerTask。同一时刻存在多个任务时,只要把该刻度对应的链表全部遍历一遍,执行其中的任务即可。
刻度不够怎么办?
1.增加round(轮次)属性:就像跑步一样,为每个任务加上round的计数器,当指针转过一圈时,round计数器减1,所以只需取出round=0的任务即需要触发的任务。
优点:可减少时间格过大造成的空间浪费
缺点:每次都需要遍历对应格子里面全部的任务列表,特别是当时间轮刻度粒度很小,任务列表特别长时,耗时将会大大增加。
2.采用层级时间轮:类似时钟一样,有时钟、分钟和秒钟。 层级时间轮可以进一步扩展为天轮、月轮甚至年轮。
比如对于分钟级时间轮来说,delayMs为1秒和delayMs为59秒的都已经过期,我们将其取出,再扔进底层的时间轮, delayMs为1秒的会被扔到秒级时间轮的下一个执行槽中,而59秒的会被扔到秒级时间轮的后59个时间槽中。
三、XXL-JOB定时任务中的时间轮
其实不仅仅是延迟消息会使用到时间轮,定时任务中也同样使用了时间轮算法进行任务的触发执行。
下面看看xxl-job任务触发和执行的总体流程图:
其中定时触发流程:
在scheduleThread将数据存入ringThread的时候,使用的就是时间轮算法的思想:
- 时间轮数据结构ringData: Map<Integer, List> , key是hash计算触发时间获得的秒数(1-60),value是任务id列表
- 入轮:扫描任务触发时 (1)本次任务处理完成,但下一次触发时间是在5秒内(2)本次任务未达到触发时间
- 出轮:获取当前时间秒数,从时间轮内移出当前秒数前2个秒数的任务id列表, 依次进行触发任务;(避免处理耗时太长,跨过刻度,多向前校验一秒)
- 增加时间轮的目的是:任务过多可能会延迟,为了保障触发时间尽可能和 任务设置的触发时间尽量一致,把即将要触发的任务提前放到时间轮里,每秒来触发时间轮相应节点的任务。
参考资料
https://www.cnblogs.com/wanghongsen/p/12510533.html