什么是时间轮
时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代码一段时间(这个时间越短,Timer的精度越高)。并用一个链表报错在该格子上的到期任务,同时一个指针随着时间一格一格转动,并执行相应格子中的到期任务。任务通过取摸决定放入那个格子。如下图所示:
以上图为例,假设一个格子是1秒,则整个wheel能表示的时间段为8s,假如当前指针指向2,此时需要调度一个3s后执行的任务,显然应该加入到(2+3=5)的方格中,指针再走3次就可以执行了;如果任务要在10s后执行,应该等指针走完一个round零2格再执行,因此应放入4,同时将round(1)保存到任务中。检查到期任务时应当只执行round为0的,格子上其他任务的round应减1。
使用场景
- 延迟队列,订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单。
- 超时控制(xx分钟没有动作就断开连接)
- 定时任务(5分钟后执行xx任务/每隔1天执行一次)
数据模型
public class HashWheelTimer {
private Timer timer = new Timer(); //滚动装置
private final long duration; //每隔多少时间转动
private final TimeUnit timeUnit; //时间单位
private int currentIndex; //当前数组下表
private final long wheelSize; //时间轮大小
private final Slot[] wheel; //数组表示环形
//初始化时间轮
private static Slot[] createWheel(int wheelSize);
//新建任务
public Task newTask(Runable runable,long delay, TimeUnit unit){
}
//启动时间轮
public void start(){
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Slot slot = slots[currentIndex++];
if(slot != null && slot.hasTask()){
//TODO something do slot
}
if(currentIndex >= wheelSize ){
currentIndex = 0;
}
}
},1,timeUnit.toMillis(tickDuration));
}
//时间轮中的格子
public class Slot {
private Set<Task> tasks = new HashSet<>();
//添加任务
public synchronized void addTask(Task task);
//是否有任务
public boolean hasTask();
}
//任务
public class Task {
private Runable runable;
private int cycleNum; //第几圈允许任务
}
}
以上是我写的模型的抽离,具体实现可以参考Netty的HashedWheelTimer
案例
很多时候,业务有“在一段时间之后,完成一个工作任务”的需求。
例如:滴滴打车订单完成后,如果用户一直不评价,48小时后会将自动评价为5星。
一般来说怎么实现这类“48小时后自动评价为5星”需求呢?
常见方案:启动一个cron定时任务,每小时跑一次,将完成时间超过48小时的订单取出,置为5星,并把评价状态置为已评价。
假设订单表的结构为:t_order(oid, finish_time, stars, status, …),更具体的,定时任务每隔一个小时会这么做一次:
select oid from t_order where finish_time > 48hours and status=0;
update t_order set stars=5 and status=1 where oid in[…];
如果数据量很大,需要分页查询,分页update,这将会是一个for循环。
方案的不足:
(1)轮询效率比较低
(2)每次扫库,已经被执行过记录,仍然会被扫描(只是不会出现在结果集中),有重复计算的嫌疑
(3)时效性不够好,如果每小时轮询一次,最差的情况下,时间误差会达到1小时
(4)如果通过增加cron轮询频率来减少(3)中的时间误差,(1)中轮询低效和(2)中重复计算的问题会进一步凸显