时间轮

什么是时间轮

时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代码一段时间(这个时间越短,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)中重复计算的问题会进一步凸显

猜你喜欢

转载自blog.csdn.net/u012092620/article/details/79951434