时间轮TimeWheel算法
想象这样一种情况,业务中要求提交定时任务,一种实现是提交任务后启动一个定时线程,轮训检测该任务,当任务量变的庞大的时候,这种开销是可怕的,令一种实现是将所有任务有些组织,只用一个线程就可以控制所有的定时任务,时间轮算法就是这种实现,netty、kafka、zookeeper中都使用了该算法实现延时处理。
时间轮如图所示,图中0~8代表时间周期,每个时间节点代表一个小的时间间隔,假设时间间隔为1分钟,则图中每个时间节点中将包含在这一分钟内的所有定时任务,存储为一个双向的linkedList,且图中的时钟周期为9分钟。
如图,当当前时间到达时间节点2时,时间节点1中的任务已经全部过期且处理完成,时间节点2对应的定时任务开始过期,开始处理节点2中对应的任务列表。
时间轮算法有其对应的优缺点,优点是我们可以使用一个线程监控很多的定时任务,缺点是时间粒度由节点间隔确定,所有的任务的时间间隔需要以同样的粒度定义,比如时间间隔是1小时,则我们定义定时任务的单位就为小时,无法精确到分钟和秒。
时间轮所能容纳的定时任务的时间是有限制的,由时间轮的周期决定,当超过了时间轮的时间周期,我们的定时任务该如何处理,有以下处理方式:
- 时间轮复用,比如当前时间为2,时间节点0和1的任务已过期,在此时来了一个任务,要求延时为9,此时2+9=11,对应的位置载1处,因此可以直接把任务放在1的链表,等下个时间周期继续处理。
- 将任务按照时间周期分组存放,一个时间周期结束后取出下一个时间周期的任务放入时间轮进行处理。
时间轮算法实践
我们需要如下接口(我们默认时间粒度为毫秒)
TimerTask定义具体的任务逻辑
我们需要如下接口(我们默认时间粒度为毫秒)
TimerTask定义具体的任务逻辑
Timeout是对任务的管理逻辑
public interface Timeout {
Timer timer();
TimerTask task();
boolean isExpired();
}
Timer管理所有的定时任务,也就是时间轮的要实现的主要方法
public interface Timer {
Timeout newTimeOut(TimerTask task, long delay, String argv);
}
接下来进行对应的实现
任务实体,简单的输出任务序号
import java.time.LocalDateTime;
public class MyTask implements TimerTask {
int index;
MyTask(int ix) {
index = ix;
}
@Override
public void run() throws Exception {
System.out.println(LocalDateTime.now());
System.out.println("当前任务序号是:" + index);
}
}
对任务进行管理的Timeout实体
public class MyTimeout implements Timeout {
Timer timer;
TimerTask timerTask;
String argv;
long delay;
int state;
long round;
public MyTimeout(Timer timer, TimerTask task, long delay, String argv) {
this.timer = timer;
this.timerTask = task;
this.delay = delay;
this.argv = argv;
state = 0;
}
@Override
public Timer timer() {
return timer;
}
@Override
public TimerTask task() {
return timerTask;
}
@Override
public boolean isExpired() {
return state != 0;
}
}
时间轮的实现,保留对新加任务的添加,时间轮的关闭,以及定时任务线程worker的实现逻辑
package com.example.test.simpleTimeWheel;
import org.springframework.util.CollectionUtils;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class TimeWheel implements Timer {
long basicTime;
List<Timeout> timeoutList;
CountDownLatch countDownLatch;
Worker woker = new Worker();
int wheelState = 1; // 0 运行 1 停止
int curIndex;
int size;
int mask;
long durationOfSlot;
LinkedList<Timeout>[] slots;
ArrayList<TimerTask> res = new ArrayList<>();
TimeWheel(int size, long durationOfSlot) throws Exception{
if (size < 0 || size > Integer.MAX_VALUE) {
throw new Exception("size out of range");
}
this.size = size;
slots = new LinkedList[size];
this.durationOfSlot = durationOfSlot;
mask = size - 1;
curIndex = 0;
basicTime = 0;
timeoutList = new ArrayList<>();
countDownLatch = new CountDownLatch(1);
}
@Override
public Timeout newTimeOut(TimerTask task, long delay, String argv) {
if(delay <= 0) {
try {
task.run();
} catch (Exception e) {
System.out.println("delay is less than zero, just run");
}
}
if (wheelState == 1) {
wheelState = 0;
Thread thread = new Thread(woker);
thread.start();
}
startTimeWheel();
Timeout timeout = new MyTimeout(this, task, delay, argv);
timeoutList.add(timeout);
return timeout;
}
private void startTimeWheel() {
while (basicTime == 0) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
System.out.println("countdown await exception");
}
}
}
private class Worker implements Runnable {
@Override
public void run() {
if (basicTime == 0) {
System.out.println("时间轮启动......");
basicTime = Instant.now().toEpochMilli();
countDownLatch.countDown();
}
do {
long deadline = durationOfSlot * (curIndex + 1);
for (;;) {
//System.out.println(basicTime+"basc");
long duration = Instant.now().toEpochMilli() - basicTime;
//System.out.println(deadline +" " + duration);
if (duration > deadline) {
//时间到
process(timeoutList);
LinkedList<Timeout> tasks = slots[curIndex];
process(tasks);
break;
}
}
if (curIndex == mask) {
basicTime = System.currentTimeMillis();
}
curIndex = (++curIndex) % size;
checkStop();
} while (wheelState == 0);
}
private void checkStop() {
if (!CollectionUtils.isEmpty(timeoutList)) {
return;
}
for (LinkedList l : slots) {
if (!CollectionUtils.isEmpty(l)) {
return;
}
}
System.out.println("没有定时任务,结束");
wheelState = 1;
}
private void process(List<Timeout> timeoutList) {
//System.out.println("处理新加入的任务");
for (Timeout out : timeoutList) {
MyTimeout mo = (MyTimeout) out;
if (mo.isExpired()) {
continue;
}
long needskipslots = mo.delay / durationOfSlot;
mo.round = (needskipslots - curIndex) / size;
int index = (int)(needskipslots) % size;
int i = (index + curIndex) % size;
LinkedList<Timeout> slot = slots[i];
if (slot == null) {
slot = new LinkedList<>();
}
slot.add(mo);
slots[(index + curIndex) % size] = slot;
}
timeoutList.clear();
}
void process(LinkedList<Timeout> outs) {
//System.out.println("处理到期任务");
if (CollectionUtils.isEmpty(outs)) {
return;
}
Iterator<Timeout> iterator = outs.iterator();
while (iterator.hasNext()) {
Timeout next = iterator.next();
try {
MyTimeout mo = (MyTimeout) next;
if (mo.round > 0) {
mo.round --;
continue;
}
mo.task().run();
res.add(mo.task());
iterator.remove();
System.out.println("-************************************");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
}
}
Main()方法
package com.example.test.simpleTimeWheel;
public class TimeMain {
public static void main(String[] args) throws Exception {
TimeWheel timeWheel = new TimeWheel(10, 1000);
long delayTime[] = {9000, 3000, 5000, 1000, 25000, 12000};
for (int i = 0; i < delayTime.length; i++) {
MyTask task = new MyTask(i);
timeWheel.newTimeOut(task, delayTime[i], "");
}
}
}
生成6个任务,根据延时时长(单位:毫秒),根据代码可知,我们的执行顺序将会是:3-1-2-0-5-4
具体看结果,验证了我们的想法。
Connected to the target VM, address: '127.0.0.1:5120', transport: 'socket'
时间轮启动......
2020-09-12T11:18:22.440
当前任务序号是:3
-************************************
2020-09-12T11:18:24.386
当前任务序号是:1
-************************************
2020-09-12T11:18:26.386
当前任务序号是:2
-************************************
2020-09-12T11:18:30.386
当前任务序号是:0
-************************************
2020-09-12T11:18:33.387
当前任务序号是:5
-************************************
2020-09-12T11:18:46.388
当前任务序号是:4
-************************************
没有定时任务,结束
Disconnected from the target VM, address: '127.0.0.1:5120', transport: 'socket'