• dubbo-详解海量定时任务实现机制


    1. 概念介绍

    时间轮是一种高效的、批量管理定时任务的调度模型。时间轮一般会实现成一个环形结构,类似一个时钟,分为很多槽,一个槽代表一个时间间隔,每个槽使用双向链表存储定时任务;指针周期性地跳动,跳动到一个槽位,就执行该槽位的定时任务。
    特别注意单层时间轮的容量和精度都是有限的,对于精度要求特别高、时间跨度特别大或是海量定时任务需要调度的场景,通常会使用多级时间轮以及持久化存储与时间轮结合的方案。
    Dubbo 的时间轮实现位于 dubbo-common 模块的 org.apache.dubbo.common.timer 包中。

    2. dubbo时间轮核心接口

    dubbo时间轮的一个概图:在这里插入图片描述

    3.根据如上的时间轮结构图讲解核心类

    在 Dubbo 中,所有的定时任务都要继承 TimerTask 接口。TimerTask 接口只定义了一个 run() 方法,该方法的入参是一个 Timeout 接口的对象。

    TimeTask接口定义:

    public interface TimerTask {
        void run(Timeout timeout) throws Exception;
    }
    

    Timeout 对象与 TimerTask 对象一一对应。通过 Timeout 对象,我们不仅可以查看定时任务的状态,还可以操作定时任务。TimeOut接口定义:

    public interface Timeout {
        Timer timer();
        TimerTask task();
        boolean isExpired();
        boolean isCancelled();
        boolean cancel();
    }
    

    Timer 接口定义了定时器的基本行为,其核心是 newTimeout() 方法提交一个定时任务(TimerTask)并返回关联的 Timeout 对象。接口定义为:

    public interface Timer {
        Timeout newTimeout(TimerTask task, long delay, TimeUnit unit);
        Set<Timeout> stop();
        boolean isStop();
    }
    

    HashedWheelTimeout 是 Timeout 接口的唯一实现,是 HashedWheelTimer 的内部类。HashedWheelTimeout 扮演了两个角色:第一个是时间轮中双向链表的节点,即定时任务 TimerTask 在 HashedWheelTimer 中的容器。第二个是定时任务 TimerTask 提交到 HashedWheelTimer 之后的响应结果,用于在时间轮外部查看和控制定时任务。其类的定义和核心属性的解释如下:

    private static final class HashedWheelTimeout implements Timeout {
    
        /**
         * 指定时任务当前所处状态,可选的有三个,分别是 INIT(0)、CANCELLED(1)和 EXPIRED(2)
         */
        private static final int ST_INIT = 0;
        private static final int ST_CANCELLED = 1;
        private static final int ST_EXPIRED = 2;
        private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER =
                AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");
    
        private final HashedWheelTimer timer;
        /**
         * 实际被调度的任务
         */
        private final TimerTask task;
        /**
         * 指定时任务执行的时间。这个时间是在创建 HashedWheelTimeout 时指定的,
         * 计算公式是:currentTime(创建 HashedWheelTimeout 的时间) + delay(任务延迟时间) - startTime(HashedWheelTimer 的启动时间),时间单位为纳秒
         */
        private final long deadline;
    
        @SuppressWarnings({"unused", "FieldMayBeFinal", "RedundantFieldInitialization"})
        private volatile int state = ST_INIT;
    
        /**
         * RemainingRounds will be calculated and set by Worker.transferTimeoutsToBuckets() before the
         * HashedWheelTimeout will be added to the correct HashedWheelBucket.
         * 指当前任务剩余的时钟周期数。时间轮所能表示的时间长度是有限的,
         * 在任务到期时间与当前时刻的时间差,超过时间轮单圈能表示的时长,
         * 就出现了套圈的情况,需要该字段值表示剩余的时钟周期
         */
        long remainingRounds;
    
        /**
         * This will be used to chain timeouts in HashedWheelTimerBucket via a double-linked-list.
         * As only the workerThread will act on it there is no need for synchronization / volatile.
         * 当前定时任务在链表中的前驱节点
         *
         */
        HashedWheelTimeout next;
        /**
         * 当前定时任务在链表中的后继节点
         */
        HashedWheelTimeout prev;
    
        /**
         * The bucket to which the timeout was added
         */
        HashedWheelBucket bucket;
    
    	// 核心方法省略实现,方法功能方如方法名见名知意
        public Timer timer() {}
        public TimerTask task() {}
    	public boolean cancel() {}
    	void remove() {}
    	public boolean compareAndSetState(int expected, int state) {}
    	public int state() {}
    	public boolean isCancelled() {}
    	public boolean isExpired() {}
    	public void expire() {}
    
    }
    

    HashedWheelBucket 是时间轮中的一个槽,时间轮中的槽实际上就是一个用于缓存和管理双向链表的容器,双向链表中的每一个节点就是一个 HashedWheelTimeout 对象,也就关联了一个 TimerTask 定时任务。HashedWheelBucket 持有双向链表的首尾两个节点,分别是 head 和 tail 两个字段,再加上每个 HashedWheelTimeout 节点均持有前驱和后继的引用,这样就可以正向或是逆向遍历整个双向链表了。

    HashedWheelTimer 是 Timer 接口的实现,它通过时间轮算法实现了一个定时器。HashedWheelTimer 会根据当前时间轮指针选定对应的槽(HashedWheelBucket),从双向链表的头部开始迭代,对每个定时任务(HashedWheelTimeout)进行计算,属于当前时钟周期则取出运行,不属于则将其剩余的时钟周期数减一操作。时间轮对外提供了一个 newTimeout() 接口用于提交定时任务,在定时任务进入到 timeouts 队列之前会先调用 start() 方法启动时间轮,其中会完成下面两个关键步骤:

    确定时间轮的 startTime 字段;
    启动 workerThread 线程,开始执行 worker 任务。

    然后根据 startTime 计算该定时任务的 deadline 字段,最后才能将定时任务封装成 HashedWheelTimeout 并添加到 timeouts 队列。
    时间轮指针一次转动的全流程:

    • 时间轮指针转动,时间轮周期开始。

    • 清理用户主动取消的定时任务,这些定时任务在用户取消时,会记录到 cancelledTimeouts 队列中。在每次指针转动的时候,时间轮都会清理该队列。

    • 将缓存在 timeouts 队列中的定时任务转移到时间轮中对应的槽中。

    • 根据当前指针定位对应槽,处理该槽位的双向链表中的定时任务。

    • 检测时间轮的状态。如果时间轮处于运行状态,则循环执行上述步骤,不断执行定时任务。如果时间轮处于停止状态,则执行下面的步骤获取到未被执行的定时任务并加入 unprocessedTimeouts 队列:遍历时间轮中每个槽位,并调用 clearTimeouts() 方法;对 timeouts 队列中未被加入槽中循环调用 poll()。

    • 最后再次清理 cancelledTimeouts 队列中用户主动取消的定时任务。

  • 相关阅读:
    部署LVS-NAT群集实验
    科大讯飞分类算法挑战赛2023的一些经验总结
    Scratch软件编程等级考试一级——201909
    webrtc USB摄像头
    coding持续集成
    JAVA毕业设计HTML5果蔬经营平台计算机源码+lw文档+系统+调试部署+数据库
    1.2-断言
    Java毕业设计-社区诊所管理系统
    工厂方法(Factory Methods),抽象工厂(Abstract Factory )
    通信设备为什么需要接地阻?
  • 原文地址:https://blog.csdn.net/tdt008/article/details/127089295