定时任务----时间轮算法
背景
在实际的业务场景中,我们常常需要周期性执行一些任务,比如巡查系统资源,处理过期数据等等。这些事情如果人工去执行的话,无疑是对人力资源的浪费。因此我们就开发出了定时任务。目前业界已有许多出色的定时任务框架,如quartz,elastic-job,包括SpringBoot也提供了定时任务,当然JDK本身也提供了定时任务功能。
那么我们在用这些框架的时候,有没有想过它们是怎么实现定时任务的呢?时间轮算法就是这样一种实现定时任务的方法。
概述
时间轮算法是通过一个时间轮去维护定时任务,按照一定的时间单位对时间轮进行划分刻度。然后根据任务的延时计算任务该落在时间轮的第几个刻度,如果任务时长超出了时间轮的刻度数量,则增加一个参数记录时间轮需要转动的圈数。
时间轮每转动一次就检查当前刻度下的任务圈数是否为0,如果为0说明时间到了就执行任务,否则就减少任务的圈数。这样看起来已经很好了,可以满足基本的定时任务需求了,但是我们还能不能继续优化一下呢?答案是可以的。想想我们家里的水表,它是不是有多个轮子在转动,时间轮是不是也可以改造成多级联动呢?建立3个时间轮,月轮、周轮、日轮,月轮存储每个月份需要执行定时任务,转动时将当月份的任务抛到周轮,周轮转动时将当天的任务抛到日轮中,日轮转动时直接执行当前刻度下的定时任务。
研究分析
笔者从github找了一个时间轮的项目,我们来研究一下时间轮的具体实现是怎样的。
https://github.com/ifesdjeen/hashed-wheel-timer.git
clone下来的项目目录是这样的,我们只需要查看core模块下的几个类就ok了。
HashedWheelTimer类就是实现时间轮的类,由于篇幅有限,下面只选取一些重要代码片段讲解
时间轮属性
public class HashedWheelTimer implements ScheduledExecutorService {
//时间轮默认转动一次的时间,10毫秒
public static final long DEFAULT_RESOLUTION = TimeUnit.NANOSECONDS.convert(10, TimeUnit.MILLISECONDS);
//时间轮默认尺寸
public static final int DEFAULT_WHEEL_SIZE = 512;
private static final String DEFAULT_TIMER_NAME = "hashed-wheel-timer";
//时间轮
private final Set<Registration<?>>[] wheel;
//时间轮尺寸
private final int wheelSize;
//转动一次花费的时间
private final long resolution;
//运转时间轮线程的执行器
private final ExecutorService loop;
//运转定时任务的执行器
private final ExecutorService executor;
//时间轮转动的等待策略
private final WaitStrategy waitStrategy;
//当前时间轮所在的刻度
private volatile int cursor = 0;
构造时间轮
public HashedWheelTimer(String name, long res, int wheelSize, WaitStrategy strategy, ExecutorService exec) {
this.waitStrategy = strategy;
this.wheel = new Set[wheelSize];
for (int i = 0; i < wheelSize; i++) {
wheel[i] = new ConcurrentSkipListSet<>();
}
this.wheelSize = wheelSize;
this.resolution = res;
final Runnable loopRunnable = new Runnable() {