dubbo-定时任务利器时间轮

685 阅读4分钟

背景

说到定时任务,笔者接触过的就有crontable,spirng-schedule,quartz 实现有很多方式,但是底层都依赖操作系统的中断,今天我们要介绍一款比较实用,并且面试加分的定时任务时间轮,时间轮算法出自一篇论文《Hashed and Hierarchical Timing Wheels》,当然在java框架中也有大牛落地,比如netty,dubbo等。

设计

我们先说说他的设计,设计和现实生活中的时钟很接近,分钟指针从0匀速转到12,是一个小时,然后我们联想下hashmap,如果hash槽长度是12,每个槽用list存储定时任务。接下来我们去实现一种比较简单的方法。

设计图

说明下上图的结构,有几个概念,任务管理器:MyHashedWheelTimer 是任务提交和执行的容器,任务队列:HashedWheelBucket 是同一个hash槽中的任务组装容器,HashedWheelTimeout 包装了每个任务扩展pre与next和任务执行时间等,里面包含了最小原子的任务TimerTask,一个周期中指针跳动一次的时间长度为tickDuration

代码编写

我们根据图上的设计把几个类模型建立下。

public class MyHashedWheelTimer {
    //调用定时任务的执行对象
    private final Worker worker = new Worker();
    /**
     * 定时任务管理器开始的毫秒时刻
     */
    private volatile long startTime;
    //指针运行一圈走过的点数
    private final int mask;
    private final HashedWheelBucket[] wheel;
    /**
     * 时钟转动的最小单位 毫秒
     */
    private final long tickDuration;
    //根据指针跳转一次的时间来初始化 任务管理器
    public MyHashedWheelTimer(long tickDuration) {}
    //将超时任务`task` 添加到任务管理器 以供后续触发
    public HashedWheelTimeout newTimeout(Runnable task, long delay, TimeUnit unit){}
}

private static final class HashedWheelBucket {
    private HashedWheelTimeout head;//队列头
    private HashedWheelTimeout tail;//队列尾
	//将超时任务添加到hash槽的队列中
    void addTimeout(HashedWheelTimeout timeout) {}
	//执行超时任务
    void doTimeouts(long deadline, long tick) {}
	//执行完任务,移除任务
    public HashedWheelTimeout remove(HashedWheelTimeout timeout) {}
}

private final class Worker implements Runnable {
    private long tick;//指针跳转的次数,随着任务管理器启动之后就一直叠加

    @Override
    public void run() {
        do {
            final long deadline = waitForNextTick();//指针每跳一次之后的时间
			//...找到对应任务,然后执行
        } while (true);
    }

    private long waitForNextTick() {
        //指针跳转一次
    }

}

//将任务和任务的执行时间包装
private static final class HashedWheelTimeout {
    long remainingRounds;//任务会在时钟从启动到转到多少次后执行
    HashedWheelTimeout next;
    HashedWheelTimeout prev;
    HashedWheelBucket bucket;
    long deadline;
    Runnable task;
    HashedWheelTimeout(Runnable task, long deadline) {
        this.task = task;
        this.deadline = deadline;
    }
}

上面4个类实现了最基本的功能,下面我们补充下具体实现

public MyHashedWheelTimer(long tickDuration) {
    wheel = createWheel(64);//默认一周期 64个单元
    mask = wheel.length - 1;//查找任务所在的hash环位置用 例如 a & mask = idx
    this.tickDuration = TimeUnit.MILLISECONDS.toMillis(tickDuration);//跳转一次的时间
    startTime = System.currentTimeMillis();//任务管理器启动事件
    new Thread(worker).start();//启动任务调用线程
}
//创建每个hash槽上的HashedWheelBucket对象
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
    HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
    for (int i = 0; i < wheel.length; i++) {
        wheel[i] = new HashedWheelBucket();
    }
    return wheel;
}

public HashedWheelTimeout newTimeout(Runnable task, long delay, TimeUnit unit) {
    //次任务在未来哪一毫秒执行
    long deadline = System.currentTimeMillis() + unit.toMillis(delay) - startTime;
    HashedWheelTimeout timeout = new HashedWheelTimeout(task, deadline);
    //任务执行的时间距离任务管理器启动需要多少个周期
    timeout.remainingRounds = (deadline / tickDuration);
    //任务将会落在hash槽的什么位置索引上
    int stopIndex = (int) ((deadline / tickDuration) & mask);
    HashedWheelBucket bucket = wheel[stopIndex];
    bucket.addTimeout(timeout);//将任务添加到hash槽的链表中
    return timeout;
}

private final class Worker implements Runnable {
    public void run() {
        do {
            final long deadline = waitForNextTick();
            if (deadline > 0) {
                int idx = (int) (tick & mask); //  & 64
                HashedWheelBucket bucket =
                        wheel[idx];
                bucket.doTimeouts(deadline, tick);//执行该hash槽中满足条件的定时任务
                tick++;
            }
        } while (true);
    }
}

void addTimeout(HashedWheelTimeout timeout) {
    //双向链表添加节点,不懂可以去看看数据结构操作
}
public HashedWheelTimeout remove(HashedWheelTimeout timeout) {
	//双向链表删除节点,不懂可以去看看数据结构操作  
}
//遍历链表中的所有任务
void doTimeouts(long deadline, long tick) {
            HashedWheelTimeout timeout = head;
    while (timeout != null) {
        HashedWheelTimeout next = timeout.next;
        //如果满足remainingRounds(需要指针跳转的次数)-tick(已经跳转的次数) <=0 则触发任务执行
        if (timeout.remainingRounds - tick <= 0) {
            next = remove(timeout);
            if (timeout.deadline <= deadline) {
                timeout.task.run();//调用任务的run()方法,和线程池有异曲同工之妙
            } else {
                throw new IllegalStateException();
            }
        }
        timeout = next;
    }
}

分析

代码中只是基础功能的实现,具体大家可以看看和dubbo 或者 netty 的实现差别在哪,比如任务管理没有启停,task提交了不可以取消,线程不安全,入参不够丰富,任务个数不可控等等。

思考

  • 时间轮对执行的任务只会处理一次,怎么实现周期性的任务?
  • 如果任务还需要等待很长时间才执行怎么设计?
  • 如果任务很多很密集怎么让任务最接近理论执行时间?

总结

代码不全,如需要完整代码可以留言。