定时任务原理方案综述 | 京东云技术团队

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

本文主要介绍目前存在的定时任务处理解决方案。业务系统中存在众多的任务需要定时或定期执行,并且针对不同的系统架构也需要提供不同的解决方案。京东内部也提供了众多定时任务中间件来支持,总结当前各种定时任务原理,从定时任务基础原理、单机定时任务(单线程、多线程)、分布式定时任务介绍目前主流的定时任务的基本原理组成、优缺点等。希望能帮助读者深入理解定时任务具体的算法和实现方案。

一、背景概述

定时任务,顾名思义,就是指定时间点进行执行相应的任务。业务场景中包括:

  1. 每天晚上12点,将当日的销售数据发送给各个VP;

  2. 订单下单十分钟未付款将自动取消订单;用户下单后发短信;

  3. 定时的清理系统中失效的数据;

  4. 心跳检测、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问题:

  1. 任务执行时间长影响其他任务:如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。Timer线程并不捕获异常,所以 TimerTask抛出的未检查的异常会终止timer线程。此时,已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。

  2. 任务异常影响其他任务:Timer里面的任务如果执行时间太长,会独占Timer对象,使得后面的任务无法几时的执行 ,ScheduledExecutorService不会出现Timer的问题(除非你只搞一个单线程池的任务区)。

3.1.3 DelayQueue

DelayQueue 是一个支持延时获取元素的无界阻塞队列,DelayQueue 其实就是在每次往优先级队列中添加元素,然后以元素的delay过期值作为排序的因素,以此来达到先过期的元素会拍在队首,每次从队列里取出来都是最先要过期的元素。

  1. delayed是一个具有过期时间的元素

  2. PriorityQueue是一个根据队列里元素某些属性排列先后的顺序队列(核心还是基于小顶堆)

队列中的元素必须实现 Delayed 接口,并重写 getDelay(TimeUnit) 和 compareTo(Delayed) 方法。

  1. CompareTo(Delayed o):Delayed接口继承了Comparable接口,因此有了这个方法。

  2. getDelay(TimeUnit unit):这个方法返回到激活日期的剩余时间,时间单位由单位参数指定。

队列入队出队方法:

  1. offer():入队的逻辑综合了PriorityBlockingQueue的平衡二叉堆冒泡插入以及DelayQueue的消费线程唤醒与leader领导权剥夺

  2. take():出队的逻辑一样综合了PriorityBlockingQueue的平衡二叉堆向下降级以及DelayQueue的Leader-Follower线程等待唤醒模式

在ScheduledExecutorService中推出了DelayedWorkQueue,DelayQueue队列元素必须是实现了Delayed接口的实例,而DelayedWorkQueue存放的是线程运行时代码RunnableScheduledFuture,该延时队列灵活的加入定时任务特有的方法调用。

<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值