本文主要介绍目前存在的定时任务处理解决方案。业务系统中存在众多的任务需要定时或定期执行,并且针对不同的系统架构也需要提供不同的解决方案。京东内部也提供了众多定时任务中间件来支持,总结当前各种定时任务原理,从定时任务基础原理、单机定时任务(单线程、多线程)、分布式定时任务介绍目前主流的定时任务的基本原理组成、优缺点等。希望能帮助读者深入理解定时任务具体的算法和实现方案。
一、背景概述
定时任务,顾名思义,就是指定时间点进行执行相应的任务。业务场景中包括:
-
每天晚上12点,将当日的销售数据发送给各个VP;
-
订单下单十分钟未付款将自动取消订单;用户下单后发短信;
-
定时的清理系统中失效的数据;
-
心跳检测、session、请求是否timeout。
二、定时任务基础原理
2.1 小顶堆算法
每个节点是对应的定时任务,定时任务的执行顺序通过利用堆化进行排序,循环判断每秒是否堆顶的任务是否应该执行,每次插入任务、删除任务需要重新堆化;

图1 利用小顶堆来获取需要最新执行的任务
为什么用优先队列(小顶堆)而不是有序的数组或者链表?
因为优先队列只需要确保局部有序,它的插入、删除操作的复杂度都是O(log n);而有序数组的插入和删除复杂度为O(n);链表的插入复杂度为O(n),删除复杂度为O(1)。总体而言优先队列性能最好。
2.2 时间轮算法
链表或者数组实现时间轮:

图2 利用链表+数组实现时间轮算法
round时间轮: 时间轮其实就是一种环型的数据结构,可以把它想象成一个时钟,分成了许多格子,每个格子代表一定的时间,在这个格子上用一个链表来保存要执行的超时任务,同时有一个指针一格一格的走,走到那个格子时就执行格子对应的延迟任务。

图3 环形数据结构的round时间轮
2.3 分层时间轮
就是将月、周、天分成不同的时间轮层级,各自的时间轮进行定义:

图4 按时间维度分层的时间轮
三、单机定时任务
3.1 单线程任务调度
3.1.1 无限循环
创建thread,在while中一直执行,通过sleep来达到定时任务的效果。
3.1.2 JDK提供了Timer
Timer位于java.util包下,其内部包含且仅包含一个后台线程(TimeThread)对多个业务任务(TimeTask)进行定时定频率的调度。

图5 JDK中Timer支持的调度方法
每个Timer中包含一个TaskQueue对象,这个队列存储了所有将被调度的task, 该队列是一个根据task下一次运行时间排序形成的最小优先队列,该最小优先队列的是一个二叉堆,所以可以在log(n)的时间内完成增加task,删除task等操作,并且可以在常数时间内获得下次运行时间最小的task对象。
原理: TimerTask是按nextExecutionTime进行堆排序的。每次取堆中nextExecutionTime和当前系统时间进行比较,如果当前时间大于nextExecutionTime则执行,如果是单次任务,会将任务从最小堆,移除。否则,更新nextExecutionTime的值。

图6 TimerTask中按照时间的堆排序
任务追赶特性:
schedule在执行的时候,如果Date过了,也就是Date是小于现在时间,那么会立即执行一次,然后从这个执行时间开始每隔间隔时间执行一次;
scheduleAtFixedRate在执行的时候,如果Date过了。还会执行,然后才是每隔一段时间执行。
Timer问题:
-
任务执行时间长影响其他任务:如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。Timer线程并不捕获异常,所以 TimerTask抛出的未检查的异常会终止timer线程。此时,已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。
-
任务异常影响其他任务:Timer里面的任务如果执行时间太长,会独占Timer对象,使得后面的任务无法几时的执行 ,ScheduledExecutorService不会出现Timer的问题(除非你只搞一个单线程池的任务区)。
3.1.3 DelayQueue
DelayQueue 是一个支持延时获取元素的无界阻塞队列,DelayQueue 其实就是在每次往优先级队列中添加元素,然后以元素的delay过期值作为排序的因素,以此来达到先过期的元素会拍在队首,每次从队列里取出来都是最先要过期的元素。
-
delayed是一个具有过期时间的元素
-
PriorityQueue是一个根据队列里元素某些属性排列先后的顺序队列(核心还是基于小顶堆)
队列中的元素必须实现 Delayed 接口,并重写 getDelay(TimeUnit) 和 compareTo(Delayed) 方法。
-
CompareTo(Delayed o):Delayed接口继承了Comparable接口,因此有了这个方法。
-
getDelay(TimeUnit unit):这个方法返回到激活日期的剩余时间,时间单位由单位参数指定。
队列入队出队方法:
-
offer():入队的逻辑综合了PriorityBlockingQueue的平衡二叉堆冒泡插入以及DelayQueue的消费线程唤醒与leader领导权剥夺
-
take():出队的逻辑一样综合了PriorityBlockingQueue的平衡二叉堆向下降级以及DelayQueue的Leader-Follower线程等待唤醒模式
在ScheduledExecutorService中推出了DelayedWorkQueue,DelayQueue队列元素必须是实现了Delayed接口的实例,而DelayedWorkQueue存放的是线程运行时代码RunnableScheduledFuture,该延时队列灵活的加入定时任务特有的方法调用。
<

本文介绍了定时任务的背景、基础原理,包括小顶堆和时间轮算法,并详细讲解了单机定时任务的各种实现,如JDKTimer、DelayQueue、ScheduledExecutorService,以及多线程调度。此外,还探讨了分布式定时任务的实现,如Redis的ZSet、Elastic-job和xxl-job。总结了各种方案的优缺点,以帮助读者理解和选择合适的定时任务解决方案。
最低0.47元/天 解锁文章
2252

被折叠的 条评论
为什么被折叠?



