Dubbo/Netty中時間輪算法的原理

在Dubbo中,爲增強系統的容錯能力,在很多地方需要用到只需進行一次執行的任務調度。比如RPC調用的超時機制的實現,消費者需要各個RPC調用是否超時,如果超時會將超時結果返回給應用層。在Dubbo最開始的實現中,是採用將所有的返回結果(DefaultFuture)都放入一個集合中,並且通過一個定時任務,每隔一定時間間隔就掃描所有的future,逐個判斷是否超時。

這樣的實現方式實現起來比較簡單,但是存在一個問題就是會有很多無意義的遍歷操作。比如一個RPC調用的超時時間是10秒,而我的超時判定定時任務是2秒執行一次,那麼可能會有4次左右無意義的輪詢操作。

爲了解決類似的場景中的問題,Dubbo借鑑Netty,引入了時間輪算法,用來對只需要執行一次的任務進行調度。時間輪算法的原理可以參見這篇文章,https://blog.csdn.net/mindfloating/article/details/8033340

下面主要分析一下Dubbo/Netty中時間輪算法的實現。Dubbo/Netty中時間輪算法主要有以下幾個類實現:
在這裏插入圖片描述

Timer接口

/**
 * Schedules {@link TimerTask}s for one-time future execution in a background
 * thread.
 */
public interface Timer {

    /**
     * Schedules the specified {@link TimerTask} for one-time execution after
     * the specified delay.
     *
     * @return a handle which is associated with the specified task
     * @throws IllegalStateException      if this timer has been {@linkplain #stop() stopped} already
     * @throws RejectedExecutionException if the pending timeouts are too many and creating new timeout
     *                                    can cause instability in the system.
     */
    Timeout newTimeout(TimerTask task, long delay, TimeUnit unit);

    /**
     * Releases all resources acquired by this {@link Timer} and cancels all
     * tasks which were scheduled but not executed yet.
     *
     * @return the handles associated with the tasks which were canceled by
     * this method
     */
    Set<Timeout> stop();

    /**
     * the timer is stop
     *
     * @return true for stop
     */
    boolean isStop();
}

這個接口是一個調度的核心接口,從註釋可以看出,它主要用於在後臺執行一次性的調度。它有一個isStop方法,用來判斷這個調度器是否停止運行,還有一個stop方法用來停止調度器的運行。再看newTimeout這個方法,這個方法就是把一個任務扔給調度器執行,第一個參數類型TimerTask,即需要執行的任務,第二個參數類型long,即執行此任務的相對延遲時間,第三個是一個時間單位,也就是第二個參數對應的時間單位。接下來看它的入參TimerTask

TimerTask接口

/**
 * A task which is executed after the delay specified with
 * {@link Timer#newTimeout(TimerTask, long, TimeUnit)} (TimerTask, long, TimeUnit)}.
 */
public interface TimerTask {

    /**
     * Executed after the delay specified with
     * {@link Timer#newTimeout(TimerTask, long, TimeUnit)}.
     *
     * @param timeout a handle which is associated with this task
     */
    void run(Timeout timeout) throws Exception;
}

這個類就代表調度器要執行的任務,它只有一個方法run,參數類型是Timeout,我們注意到上面Timer接口的newTimeout這個方法返回的參數就是Timeout,和此處的入參相同,大膽猜測這裏傳入的Timeout參數應該就是newTimeout的返回值。(留待後文驗證)

Timeout接口

/**
 * A handle associated with a {@link TimerTask} that is returned by a
 * {@link Timer}.
 */
public interface Timeout {

    /**
     * Returns the {@link Timer} that created this handle.
     */
    Timer timer();

    /**
     * Returns the {@link TimerTask} which is associated with this handle.
     */
    TimerTask task();

    /**
     * Returns {@code true} if and only if the {@link TimerTask} associated
     * with this handle has been expired.
     */
    boolean isExpired();

    /**
     * Returns {@code true} if and only if the {@link TimerTask} associated
     * with this handle has been cancelled.
     */
    boolean isCancelled();

    /**
     * Attempts to cancel the {@link TimerTask} associated with this handle.
     * If the task has been executed or cancelled already, it will return with
     * no side effect.
     *
     * @return True if the cancellation completed successfully, otherwise false
     */
    boolean cancel();
}

Timeout代表的是對一次任務的處理。timer方法返回的就是創建這個Timeout的Timer對象,task返回的是這個Timeout處理的任務,isExpired代表的是這個任務是否已經超過它預設的時間,isCancelled是返回是否已取消此任務,cancel則是取消此任務。

以上者幾個接口就從邏輯上構成了一個任務調度器系統。我們從各個接口的入參和返回值可以看出,這幾個接口設計的很巧妙,往往是某個類創建了另一個類的對象,然後它創建的對象又可以通過方法獲取到創建它的對象。這種設計方式在spring框架中也是經常出現的。可以看出在設計一個複雜的系統時這是一種很有效的方式。可以學習一下。

下面就開始看本文的重點,時間輪調度器的實現HashedWheelTimer。首先是類頭:

/**
 * A {@link Timer} optimized for approximated I/O timeout scheduling.
 *
 * <h3>Tick Duration</h3>
 * <p>
 * As described with 'approximated', this timer does not execute the scheduled
 * {@link TimerTask} on time.  {@link HashedWheelTimer}, on every tick, will
 * check if there are any {@link TimerTask}s behind the schedule and execute
 * them.
 * <p>
 * You can increase or decrease the accuracy of the execution timing by
 * specifying smaller or larger tick duration in the constructor.  In most
 * network applications, I/O timeout does not need to be accurate.  Therefore,
 * the default tick duration is 100 milliseconds and you will not need to try
 * different configurations in most cases.
 *
 * <h3>Ticks per Wheel (Wheel Size)</h3>
 * <p>
 * {@link HashedWheelTimer} maintains a data structure called 'wheel'.
 * To put simply, a wheel is a hash table of {@link TimerTask}s whose hash
 * function is 'dead line of the task'.  The default number of ticks per wheel
 * (i.e. the size of the wheel) is 512.  You could specify a larger value
 * if you are going to schedule a lot of timeouts.
 *
 * <h3>Do not create many instances.</h3>
 * <p>
 * {@link HashedWheelTimer} creates a new thread whenever it is instantiated and
 * started.  Therefore, you should make sure to create only one instance and
 * share it across your application.  One of the common mistakes, that makes
 * your application unresponsive, is to create a new instance for every connection.
 *
 * <h3>Implementation Details</h3>
 * <p>
 * {@link HashedWheelTimer} is based on
 * <a href="http://cseweb.ucsd.edu/users/varghese/">George Varghese</a> and
 * Tony Lauck's paper,
 * <a href="http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z">'Hashed
 * and Hierarchical Timing Wheels: data structures to efficiently implement a
 * timer facility'</a>.  More comprehensive slides are located
 * <a href="http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt">here</a>.
 */
public class HashedWheelTimer implements Timer {

從註釋可以看出,該類並不提供準確的定時執行任務的功能,也就是不能指定幾點幾分幾秒準時執行某個任務,而是在每個tick(也就是時間輪的一個“時間槽”)中,檢測是否存在TimerTask已經落後於當前時間,如果是則執行它。(相信瞭解了時間輪算法的同學,應該是很容易理解這段話的意思的。)我們可以通過設定更小或更大的tick duration(時間槽的持續時間),來提高或降低執行時間的準確率。這句話也很好理解,比如我一個時間槽有1秒,和一個時間槽是5秒,那準確度相差5倍。註釋繼續說,在大多數網絡應用程序中,IO超時不必須是準確的,也就是比如說我要求5秒就超時,那框架不是說必須要在5秒剛好超時的那個點告訴我超時,也可以稍微晚一點點也無所謂。因此,默認的tick duration是100毫秒,我們在大多數場景下並不需要修改它。

這個類維護了一種稱爲“wheel”的數據結構,也就是我們說的時間輪。簡單地說,一個wheel就是一個hash table,它的hash函數是任務的截止時間,也就是我們要通過hash函數把這個任務放到它應該在的時間槽中,這樣隨着時間的推移,當我們進入某個時間槽中時,這個槽中的任務也剛好到了它該執行的時間。這樣就避免了在每一個槽中都需要檢測所有任務是否需要執行。默認的時間槽的數量是512,如果我們需要調度非常多的任務,我們可以自定義這個值。

這個類在系統中只需要創建一個實例,因爲它在每次被初始化並開始運行的時候,會創建一個新的線程。一個常見的使用錯誤是,對每個連接(這裏應該是Netty中的註釋,因爲這個類主要用在處理連接,這裏的連接可以理解爲任務)都創建一個這個類,這將導致應用程序變得不可響應(開的線程太多)。

下面就是介紹這個類的實現原理依據的論文,就不看了。下面直接看代碼。首先是field。

   /**
     * may be in spi?
     */
    public static final String NAME = "hased";

    private static final Logger logger = LoggerFactory.getLogger(HashedWheelTimer.class);

    // 實例計數器,用於記錄創建了多少個本類的對象
    private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
    // 用於對象數超過限制時的告警
    private static final AtomicBoolean WARNED_TOO_MANY_INSTANCES = new AtomicBoolean();
    // 實例上限
    private static final int INSTANCE_COUNT_LIMIT = 64;
    // 原子化更新workState變量的工具
    private static final AtomicIntegerFieldUpdater<HashedWheelTimer> WORKER_STATE_UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimer.class, "workerState");
    // 推動時間輪運轉的執行類
    private final Worker worker = new Worker();
    // 綁定的執行線程
    private final Thread workerThread;

    // WORKER初始化狀態
    private static final int WORKER_STATE_INIT = 0;
    // WORKER已開始狀態
    private static final int WORKER_STATE_STARTED = 1;
    // WORKER已停止狀態
    private static final int WORKER_STATE_SHUTDOWN = 2;

    /**
     * 0 - init, 1 - started, 2 - shut down
     */
    @SuppressWarnings({"unused", "FieldMayBeFinal"})
    private volatile int workerState;

	// 時間槽持續時間
    private final long tickDuration;
    // 時間槽數組
    private final HashedWheelBucket[] wheel;
    // 計算任務應該放到哪個時間槽時使用的掩碼
    private final int mask;
    // 線程任務同步工具
    private final CountDownLatch startTimeInitialized = new CountDownLatch(1);
    // 保存任務調度的隊列
    private final Queue<HashedWheelTimeout> timeouts = new LinkedBlockingQueue<>();
    // 已取消的任務調度隊列
    private final Queue<HashedWheelTimeout> cancelledTimeouts = new LinkedBlockingQueue<>();
    // 等待中的任務調度數量
    private final AtomicLong pendingTimeouts = new AtomicLong(0);
    // 最大等待任務調度數量
    private final long maxPendingTimeouts;
    // 時間輪的初始時間
    private volatile long startTime;

可能有部分參數的作用看不太懂,結合下文就可以看懂了。首先就看一下這個方法的構造器吧。

/**
     * Creates a new timer.
     *
     * @param threadFactory      a {@link ThreadFactory} that creates a
     *                           background {@link Thread} which is dedicated to
     *                           {@link TimerTask} execution.
     * @param tickDuration       the duration between tick
     * @param unit               the time unit of the {@code tickDuration}
     * @param ticksPerWheel      the size of the wheel
     * @param maxPendingTimeouts The maximum number of pending timeouts after which call to
     *                           {@code newTimeout} will result in
     *                           {@link java.util.concurrent.RejectedExecutionException}
     *                           being thrown. No maximum pending timeouts limit is assumed if
     *                           this value is 0 or negative.
     * @throws NullPointerException     if either of {@code threadFactory} and {@code unit} is {@code null}
     * @throws IllegalArgumentException if either of {@code tickDuration} and {@code ticksPerWheel} is &lt;= 0
     */
    public HashedWheelTimer(
            ThreadFactory threadFactory,
            long tickDuration, TimeUnit unit, int ticksPerWheel,
            long maxPendingTimeouts) {

        if (threadFactory == null) {
            throw new NullPointerException("threadFactory");
        }
        if (unit == null) {
            throw new NullPointerException("unit");
        }
        if (tickDuration <= 0) {
            throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
        }
        if (ticksPerWheel <= 0) {
            throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
        }

        // Normalize ticksPerWheel to power of two and initialize the wheel.
        wheel = createWheel(ticksPerWheel);
        mask = wheel.length - 1;

        // Convert tickDuration to nanos.
        this.tickDuration = unit.toNanos(tickDuration);

        // Prevent overflow.
        if (this.tickDuration >= Long.MAX_VALUE / wheel.length) {
            throw new IllegalArgumentException(String.format(
                    "tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
                    tickDuration, Long.MAX_VALUE / wheel.length));
        }
        workerThread = threadFactory.newThread(worker);

        this.maxPendingTimeouts = maxPendingTimeouts;

        if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
                WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
            reportTooManyInstances();
        }
    }

參數的英文註釋不再翻譯。看主要邏輯,
1.首先是校驗了參數
2.很關鍵的創建時間輪,也就是初始化下面上面提到的wheel這個數組,因爲這個數組就是代表hash表的數組。
3.初始化了mask這個掩碼,它的值爲wheel.length - 1,初始化爲這個值是爲了計算方便,後面會說到。
4.之後初始化了時間槽持續時間。並進行了溢出判斷,即如果Long類型的最大值除以時間槽的個數,得出的結果小於傳入的時間槽設定時間,會拋異常。
5.設定最大等待任務調度數
6.判斷對象數量是否超過最大限制,若超過則報告。

下面展開上面的createWheel方法

	private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
        if (ticksPerWheel <= 0) {
            throw new IllegalArgumentException(
                    "ticksPerWheel must be greater than 0: " + ticksPerWheel);
        }
        if (ticksPerWheel > 1073741824) {
            throw new IllegalArgumentException(
                    "ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);
        }

        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i++) {
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
    }

忽略基本的參數校驗,看主要流程
1.對時間槽數量進行規範化處理
2.創建時間槽數組
3.初始化時間槽數組的每個參數

對時間槽數量的規範化處理

	private static int normalizeTicksPerWheel(int ticksPerWheel) {
        int normalizedTicksPerWheel = ticksPerWheel - 1;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 1;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 2;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 4;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 8;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 16;
        return normalizedTicksPerWheel + 1;
    }

假設輸入的值是37,計算之後返回的結果爲64,可以看出此方法的作用在於,將傳入的參數修改爲大於等於它的最小的2的次冪。

HashedWheelBucket這個類就是時間槽(也可以叫桶,Bucket,一個意思)。構造它使用的是默認構造函數。對於它的實現,後面再分析。

newTimeout方法

	@Override
    public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (unit == null) {
            throw new NullPointerException("unit");
        }

        long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();

        if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
            pendingTimeouts.decrementAndGet();
            throw new RejectedExecutionException("Number of pending timeouts ("
                    + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
                    + "timeouts (" + maxPendingTimeouts + ")");
        }

        start();

        // Add the timeout to the timeout queue which will be processed on the next tick.
        // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

        // Guard against overflow.
        if (delay > 0 && deadline < 0) {
            deadline = Long.MAX_VALUE;
        }
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        timeouts.add(timeout);
        return timeout;
    }

這個方法就是向調度器添加一個待執行任務。忽略基本參數校驗,主要流程:
1.將等待任務調度數加1,若等待數量超過最大限制,則減1並拋異常
2.啓動時間輪(並不是每次都啓動,只會啓動一次,start方法裏會有判斷,後面再看)
3.計算當前任務的截止時間(也就是要執行的時間),並進行防溢出處理
4.構造一個Timeout,並放入等待任務調度隊列中

start方法

	/**
     * Starts the background thread explicitly.  The background thread will
     * start automatically on demand even if you did not call this method.
     *
     * @throws IllegalStateException if this timer has been
     *                               {@linkplain #stop() stopped} already
     */
    public void start() {
        switch (WORKER_STATE_UPDATER.get(this)) {
            case WORKER_STATE_INIT:
                if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
                    workerThread.start();
                }
                break;
            case WORKER_STATE_STARTED:
                break;
            case WORKER_STATE_SHUTDOWN:
                throw new IllegalStateException("cannot be started once stopped");
            default:
                throw new Error("Invalid WorkerState");
        }

        // Wait until the startTime is initialized by the worker.
        while (startTime == 0) {
            try {
                startTimeInitialized.await();
            } catch (InterruptedException ignore) {
                // Ignore - it will be ready very soon.
            }
        }
    }

1.獲取WORKER運行狀態,若是初始化,則更新到已啓動狀態,並啓動workThread線程,若是其他狀態,做相應處理
2.若startTime==0,則在此線程中等待workThread將startTime初始化完成

此方法也很簡單,就是啓動定時器背後的執行線程,同時利用CountLatchDown等待startTime初始化爲0,這裏爲什麼要等待爲0呢?答案就是上面的newTimeout方法中,在start之後會用到這個startTime,如果它沒有初始化完成的化,計算會有問題。

到此爲止,利用HashedWheelTimer添加一個待執行任務的主體流程已經完成。下面再看一下時間輪內部是如何運轉的。下面先看Worker這個類

Worker

fields

		private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();

        private long tick;

第一個集合參數是沒有處理的任務調度集合,第二個參數是當前執行的tick(也就是當前執行到哪個時間槽了)。

run方法

		@Override
        public void run() {
            // Initialize the startTime.
            startTime = System.nanoTime();
            if (startTime == 0) {
                // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
                startTime = 1;
            }

            // Notify the other threads waiting for the initialization at start().
            startTimeInitialized.countDown();

            do {
                final long deadline = waitForNextTick();
                if (deadline > 0) {
                    int idx = (int) (tick & mask);
                    processCancelledTasks();
                    HashedWheelBucket bucket =
                            wheel[idx];
                    transferTimeoutsToBuckets();
                    bucket.expireTimeouts(deadline);
                    tick++;
                }
            } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

            // Fill the unprocessedTimeouts so we can return them from stop() method.
            for (HashedWheelBucket bucket : wheel) {
                bucket.clearTimeouts(unprocessedTimeouts);
            }
            for (; ; ) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    break;
                }
                if (!timeout.isCancelled()) {
                    unprocessedTimeouts.add(timeout);
                }
            }
            processCancelledTasks();
        }

主要邏輯
1.初始化startTime,如果startTime爲0,則初始化爲1。這裏爲什麼要判斷是否爲0呢?我們知道,java中獲取當前時間有兩種方法,一個是System.currentTimeMillis()它返回的是國際通用時間UTC中,距離1970年1月1日零點之間的毫秒數。另一個就是這裏用的System.nanoTime(),它返回的是當前時間距離虛擬機中某個固定時間點之間的時間差,單位爲毫微秒,但這個固定時間每臺虛擬機都不一樣,所以它只能用於計算時間差。回到上面這個方法,如果執行nanoTime的時刻剛好是這個固定時間,絲毫不差,那返回值就是0。所以這裏爲了防止不知道多少分之一的可能性,需要判斷一下是否爲0。
2.因爲startTime已經初始化完成,所以startLatchDown通知等待的線程,可以繼續執行了。
3.接下來是一個for循環,當定時器一直是已啓動的狀態時,不斷地推進tick前進。推進的過程:
1)等待下一個tick的到來
2)tick到來之後,計算tick對應時間槽數組中的那個槽(這裏tick&mask,就相當於對時間槽數組的長度取模運算)
3)處理已取消任務調度隊列
4)獲取當前時間槽,並將待處理任務隊列中的任務放到它們應該放的槽中
5)當前時間槽執行它包含的任務

4.若時間輪已被停止,則執行下列流程:
1)清理所有時間槽中的未處理任務調度
2)清理待處理任務調度隊列,將未取消的加入到未處理集合中
3)處理已取消的任務調度隊列

waitForNextTick方法

		/**
         * calculate goal nanoTime from startTime and current tick number,
         * then wait until that goal has been reached.
         *
         * @return Long.MIN_VALUE if received a shutdown request,
         * current time otherwise (with Long.MIN_VALUE changed by +1)
         */
        private long waitForNextTick() {
            long deadline = tickDuration * (tick + 1);

            for (; ; ) {
                final long currentTime = System.nanoTime() - startTime;
                long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

                if (sleepTimeMs <= 0) {
                    if (currentTime == Long.MIN_VALUE) {
                        return -Long.MAX_VALUE;
                    } else {
                        return currentTime;
                    }
                }
                if (isWindows()) {
                    sleepTimeMs = sleepTimeMs / 10 * 10;
                }

                try {
                    Thread.sleep(sleepTimeMs);
                } catch (InterruptedException ignored) {
                    if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                        return Long.MIN_VALUE;
                    }
                }
            }
        }

        Set<Timeout> unprocessedTimeouts() {
            return Collections.unmodifiableSet(unprocessedTimeouts);
        }
    }

主要流程:
1.計算下一個tick的開始時間
2.循環等待直到時間到達下一個tick開始時間,這裏sleepTimeMs <= 0,等價於deadline - currentTime <= -999999(毫微秒),也就是說當前時間超過下一個tick 999999毫微秒了,纔到時間。這裏就會返回了
3.計算一個睡眠時間,然後線程睡眠一下。

processCancelledTasks方法

		private void processCancelledTasks() {
            for (; ; ) {
                HashedWheelTimeout timeout = cancelledTimeouts.poll();
                if (timeout == null) {
                    // all processed
                    break;
                }
                try {
                    timeout.remove();
                } catch (Throwable t) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("An exception was thrown while process a cancellation task", t);
                    }
                }
            }
        }

for循環:
1.從已取消隊列中取出第一個被取消的任務調度
2.調用HashedWheelTimeout的remove方法進行移除,這個方法後面再看

transferTimeoutsToBuckets方法

		private void transferTimeoutsToBuckets() {
            // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
            // adds new timeouts in a loop.
            for (int i = 0; i < 100000; i++) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    // all processed
                    break;
                }
                if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
                    // Was cancelled in the meantime.
                    continue;
                }

                long calculated = timeout.deadline / tickDuration;
                timeout.remainingRounds = (calculated - tick) / wheel.length;

                // Ensure we don't schedule for past.
                final long ticks = Math.max(calculated, tick);
                int stopIndex = (int) (ticks & mask);

                HashedWheelBucket bucket = wheel[stopIndex];
                bucket.addTimeout(timeout);
            }
        }

for循環10000次(這裏只循環有限次,是爲了防止待處理隊列過大,導致這一次添加到對應槽的過程太過耗時):
1.從待處理任務調度隊列中取出第一個任務,進行校驗
2.根據取出的待處理任務調度,計算出一個槽
3.設置此任務調度的remaininRounds(剩餘圈數),因爲時間輪是一個輪,所以可能會有還需要過幾圈的時間才能執行到的任務
4.取計算出的槽和當前槽中的較大者,並進行取模
5.將此任務調度加入對應的槽中

上面已經介紹完了時間槽運轉的主體流程。相信大家還有很多不明白的地方,下面再介紹一下HashedWheelTimeout和HashedWheelBucket這兩個類。首先是HashedWheelTimeout這個類。

HashedWheelTimeout類

	private static final class HashedWheelTimeout implements Timeout {

        // 初始化狀態
		private static final int ST_INIT = 0;
		// 已取消狀態
        private static final int ST_CANCELLED = 1;
        // 已超時狀態
        private static final int ST_EXPIRED = 2;
        // state屬性獲取器
        private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER =
                AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");
        
        // 調度器
        private final HashedWheelTimer timer;
        // 調度任務
        private final TimerTask task;
        // 截止時間
        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;

        HashedWheelTimeout(HashedWheelTimer timer, TimerTask task, long deadline) {
            this.timer = timer;
            this.task = task;
            this.deadline = deadline;
        }

        @Override
        public Timer timer() {
            return timer;
        }

        @Override
        public TimerTask task() {
            return task;
        }

        @Override
        public boolean cancel() {
            // only update the state it will be removed from HashedWheelBucket on next tick.
            if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {
                return false;
            }
            // If a task should be canceled we put this to another queue which will be processed on each tick.
            // So this means that we will have a GC latency of max. 1 tick duration which is good enough. This way
            // we can make again use of our MpscLinkedQueue and so minimize the locking / overhead as much as possible.
            timer.cancelledTimeouts.add(this);
            return true;
        }

        void remove() {
            HashedWheelBucket bucket = this.bucket;
            if (bucket != null) {
                bucket.remove(this);
            } else {
                timer.pendingTimeouts.decrementAndGet();
            }
        }

        public boolean compareAndSetState(int expected, int state) {
            return STATE_UPDATER.compareAndSet(this, expected, state);
        }

        public int state() {
            return state;
        }

        @Override
        public boolean isCancelled() {
            return state() == ST_CANCELLED;
        }

        @Override
        public boolean isExpired() {
            return state() == ST_EXPIRED;
        }

        public void expire() {
            if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
                return;
            }

            try {
                task.run(this);
            } catch (Throwable t) {
                if (logger.isWarnEnabled()) {
                    logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + '.', t);
                }
            }
        }

        @Override
        public String toString() {
            final long currentTime = System.nanoTime();
            long remaining = deadline - currentTime + timer.startTime;
            String simpleClassName = ClassUtils.simpleClassName(this.getClass());

            StringBuilder buf = new StringBuilder(192)
                    .append(simpleClassName)
                    .append('(')
                    .append("deadline: ");
            if (remaining > 0) {
                buf.append(remaining)
                        .append(" ns later");
            } else if (remaining < 0) {
                buf.append(-remaining)
                        .append(" ns ago");
            } else {
                buf.append("now");
            }

            if (isCancelled()) {
                buf.append(", cancelled");
            }

            return buf.append(", task: ")
                    .append(task())
                    .append(')')
                    .toString();
        }
    }

可以看到,這個類邏輯比較簡單,基本都是賦值或讀取值的操作,或者是委託給HashedWheelBucket這個類進行操作,就不做過多介紹,大家可以自行學習。需要注意的一點是next何prev這兩個屬性,我們知道,一個HashedWheelBucket會掛載多個HashedWheelTimeout,這個next和prev就是用於實現一個雙向鏈表的結構,這樣同屬於一個HashedWheelBucket的HashedWheelTimeout就可以以雙向鏈表的形式掛載在HashedWheelBucket上了。

HashedWheelBucket

fields:

		/**
         * Used for the linked-list datastructure
         */
        private HashedWheelTimeout head;
        private HashedWheelTimeout tail;

如上文所述,這兩個參數就是HashedWheelTimeout雙向鏈表的頭尾指針。

addTimeout和remove方法

		/**
         * Add {@link HashedWheelTimeout} to this bucket.
         */
        void addTimeout(HashedWheelTimeout timeout) {
            assert timeout.bucket == null;
            timeout.bucket = this;
            if (head == null) {
                head = tail = timeout;
            } else {
                tail.next = timeout;
                timeout.prev = tail;
                tail = timeout;
            }
        }

		public HashedWheelTimeout remove(HashedWheelTimeout timeout) {
            HashedWheelTimeout next = timeout.next;
            // remove timeout that was either processed or cancelled by updating the linked-list
            if (timeout.prev != null) {
                timeout.prev.next = next;
            }
            if (timeout.next != null) {
                timeout.next.prev = timeout.prev;
            }

            if (timeout == head) {
                // if timeout is also the tail we need to adjust the entry too
                if (timeout == tail) {
                    tail = null;
                    head = null;
                } else {
                    head = next;
                }
            } else if (timeout == tail) {
                // if the timeout is the tail modify the tail to be the prev node.
                tail = timeout.prev;
            }
            // null out prev, next and bucket to allow for GC.
            timeout.prev = null;
            timeout.next = null;
            timeout.bucket = null;
            timeout.timer.pendingTimeouts.decrementAndGet();
            return next;
        }

這兩個方法平平無奇,就是雙向鏈表的添加和刪除操作。

expireTimeouts方法

		/**
         * Expire all {@link HashedWheelTimeout}s for the given {@code deadline}.
         */
        void expireTimeouts(long deadline) {
            HashedWheelTimeout timeout = head;

            // process all timeouts
            while (timeout != null) {
                HashedWheelTimeout next = timeout.next;
                if (timeout.remainingRounds <= 0) {
                    next = remove(timeout);
                    if (timeout.deadline <= deadline) {
                        timeout.expire();
                    } else {
                        // The timeout was placed into a wrong slot. This should never happen.
                        throw new IllegalStateException(String.format(
                                "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
                    }
                } else if (timeout.isCancelled()) {
                    next = remove(timeout);
                } else {
                    timeout.remainingRounds--;
                }
                timeout = next;
            }
        }

這個方法就是實際將一個時間槽中所有掛載的任務調度執行的方法。可以看出邏輯也比較簡單,就是從頭遍歷timeout的雙向鏈表,對每一個timeout進行處理,處理的流程就是,先判斷剩餘圈數是否小於等於0,如果是,再判斷它的截止時間是否小於當前截止時間,如果小於則進行expire,實際也就是包含了執行這個任務的操作。主要邏輯就是這個,其他次要邏輯就不說了,相信看一下就能明白

總結

以上介紹了Dubbo中的時間輪定時器的原理和實現,它主要是通過Timer,Timeout,TimerTask幾個接口定義了一個定時器的模型,再通過HashedWheelTimer這個類(包括其內部類)實現了一個時間輪定時器。它對外提供了簡單易用的接口,只需要調用newTimer接口,就可以實現對只需執行一次任務的調度。通過該定時器,Dubbo在響應的場景中實現了高效的任務調度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章