1. 程式人生 > 其它 >時間輪 (史上最全)

時間輪 (史上最全)

文章很長,而且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 部落格園版 為您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 經典圖書:《Java高併發核心程式設計(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心程式設計(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心程式設計(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:尼恩Java面試寶典 最新版

面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


快取之王 Caffeine 中,涉及到100w級、1000W級、甚至億級元素的過期問題,如何進行高效能的定時排程,是一個難題。

海量定時任務管理的問題

下面的問題,來自網際網路:

一個大型內容稽核平時,在運營設定稽核了內容的通過的時間,到了這個時間之後,相關內容自動稽核通過

本是個小的需求,但是考慮到如果需要定時稽核的東西很多,這樣:

海量的定時任務排程,帶來的一系列效能問題。

海量定時任務管理的場景非常多,在實際專案中,存在大量需要定時或是延時觸發的任務,

比如電商中,延時需要檢查訂單是否支付成功,是否配送成功,定時給使用者推送提醒等等

方案一 單定時器方案

描述

把所有需要定時稽核的資源放到redis中,例如sorted set中,需要稽核通過的時間作為score值。

後臺啟動一個定時器,定時輪詢sortedSet,當score值小於當前時間,則執行任務稽核通過。

問題

這個方案在小批量資料的情況下沒有問題,

但是在大批量任務的情況下就會出現問題了,因為每次都要輪詢全量的資料,逐個判斷是否需要執行,

一旦輪詢任務執行比較長,就會出現任務無法按照定時的時間執行的問題。

方案二 一個任務一個定時器的方案

描述

每個需要定時完成的任務都啟動一個定時任務,然後等待完成之後銷燬

問題

這個方案帶來的問題很明顯,定時任務比較多的情況下,會啟動很多的執行緒,這樣伺服器會承受不了之後崩潰。

基本上不會採取這個方案。

方案三 redis的過期通知功能

描述

和方案一類似,針對每一個需要定時稽核的任務,設定過期時間,

過期時間也就是稽核通過的時間,訂閱redis的過期事件,當這個事件發生時,執行相應的稽核通過任務。

問題

這個方案來說是借用了redis這種中介軟體來實現我們的功能,這中實際上屬於redis的釋出訂閱功能中的一部分,

針對redis釋出訂閱功能是不推薦我們在生產環境中做業務操作的,

通常redis內部(例如redis叢集節點上下線,選舉等等來使用),我們業務系統使用它的這個事件會產生如下兩個問題

1、redis釋出訂閱的不穩定問題

2、redid釋出訂閱的可靠性問題

具體可以參考 https://my.oschina.net/u/2457218/blog/3065021 (redis的釋出訂閱缺陷)

方案四 分層時間輪方案

這個東西就是專為大批量定時任務管理而生。

具體論文詳見參考文獻

http://www.cs.columbia.edu/~nahum/w6998/papers/sosp87-timing-wheels.pdf

快取之王 Caffeine 中的時間輪

快取之王 Caffeine 中,涉及到100w級、1000W級、甚至億級元素的過期問題,

快取之王 Caffeine 中,涉及到100w級、1000W級、甚至億級元素的過期問題,如何進行高效能的定時排程,是一個難題。

Caffeine 使用時間輪解決這個問題,

時間輪的大致結構:


Caffeine 對時間輪的實現在TimerWheel,它是一種多層時間輪(hierarchical timing wheels )。

Caffeine 的時間輪,有五級。

看看元素加入到時間輪的schedule方法:

/**
 * Schedules a timer event for the node.
 *
 * @param node the entry in the cache
 */
public void schedule(@NonNull Node<K, V> node) {
  Node<K, V> sentinel = findBucket(node.getVariableTime());
  link(sentinel, node);
}

/**
 * Determines the bucket that the timer event should be added to.
 *
 * @param time the time when the event fires
 * @return the sentinel at the head of the bucket
 */
Node<K, V> findBucket(long time) {
  long duration = time - nanos;
  int length = wheel.length - 1;
  for (int i = 0; i < length; i++) {
    if (duration < SPANS[i + 1]) {
      long ticks = (time >>> SHIFT[i]);
      int index = (int) (ticks & (wheel[i].length - 1));
      return wheel[i][index];
    }
  }
  return wheel[length][0];
}

/** Adds the entry at the tail of the bucket's list. */
void link(Node<K, V> sentinel, Node<K, V> node) {
  node.setPreviousInVariableOrder(sentinel.getPreviousInVariableOrder());
  node.setNextInVariableOrder(sentinel);

  sentinel.getPreviousInVariableOrder().setNextInVariableOrder(node);
  sentinel.setPreviousInVariableOrder(node);
}

如果看不懂這些原始碼,沒有關係,咱們由淺入深,慢慢道來。

時間輪的基本概念

時間輪這個技術其實出來很久了,在kafka、zookeeper、Netty、Dubbo等高效能元件中都有時間輪使用的方式。

如圖,時間輪,從圖片上來看,就和手錶的表圈是一樣,所以稱為時間輪.

時間輪其實就是一種環形的資料結構,其設計參考了時鐘轉動的思維,

可以想象成時鐘,分成很多格子,一個格子代表一段時間

時間輪是由多個時間格組成,下圖中有8個時間格,每個時間格代表當前時間輪的基本時間跨度(tickDuration),其中時間輪的時間格的個數是固定的。

圖中,有8個時間格(槽),假設每個時間格的單位為100ms,那麼整個時間輪走完一圈需要800ms。

每100ms指標會沿著順時針方向移動一個時間單位,

這個單位可以代表時間精度

這個單位可以設定,

比如以秒為單位,也可以以一小時為單位。

通過指標移動,來獲得每個時間格中的任務列表,然後遍歷這一個時間格中的雙向連結串列來執行任務,以此迴圈。

時間輪是以時間作為刻度, 組成的一個環形佇列,這個環形佇列採用陣列來實現,

陣列的每個元素稱為槽 Bucket,

每個槽位可以放一個定時任務列表,叫HashedWheelBucket,

每個槽位可以是一個雙向連結串列,其中可以設定一個 sentinel 哨兵節點, 作為新增任務和刪除任務的起始節點。

槽位連結串列的每一項表示一個定時任務項(HashedWhellTimeout),其中封裝了真正的定時任務TimerTask。

簡單來說:

時間輪是一種高效利用執行緒資源進行批量化排程的一種排程模型。

把大批量的排程任務全部繫結到同一個排程器上,使用這一個排程器來進行所有任務的管理、觸發、以及執行。

時間輪的模型能夠高效管理各種任務:

  • 延時任務、
  • 週期任務、
  • 通知任務。

時間輪演算法在很多框架中都有用到,比如 Dubbo、Netty、Kafka 等。

時間輪演算法也是一個比較經典的設計。

時間輪和hashmap的類比

更通用的情況, 同一時刻可能需要執行多個任務,比如:

  • 每天上午九點除了生成報表之外,
  • 執行傳送郵件的任務,
  • 執行建立檔案的任務,
  • 執行資料分析的任務
  • 等等,

為了儲存這些任務,如果有多個任務需要執行呢?

一個槽位可以指向一個數組或者連結串列,用來存放該刻度需要執行的任務,

所以,本質上,

時間輪的資料結構, 其實類似hashmap

時間輪每一個時間刻度,可以理解為一個槽位,同一時刻存在多個任務 ,放在雙向連結串列中。

如下圖所示:

和hashmap不同的是,時間輪的key,是時間刻度值,並且,時間輪不做hash運算

時間輪在同一時刻存在多個任務時,只要把該刻度對應的連結串列全部遍歷一遍,執行其中的任務即可。

當然, 時鐘排程的執行緒,和 執行任務的執行緒,一般是需要解耦的。

所以,一般來說,具體的任務,會扔到執行緒池中非同步執行。

時間刻度不夠用怎麼辦?

如果任務不只限定在一天之內呢?

比如我有個任務,需要每週一上午九點執行,我還有另一個任務,需要每週三的上午九點執行。

大概的解決辦法是:

  • 增大時間輪的刻度
  • 列表中的任務中新增round屬性
  • 分層時間輪

增大時間輪的刻度

一個刻度一小時, 一天24個小時,一週168個小時,為了解決時間刻度不夠用的問題

我可以把時間輪的刻度(槽)從12個增加到168個,

這樣的話,一週的所有時間,都可以用一個時間輪來管理。

比如,現在是星期二上午10點,

那麼,下週一上午九點,就是時間輪的第9個刻度,

那麼,下週三上午九點就是時間輪的第57個刻度,

示意圖如下:

單級時間輪的問題

仔細思考一下,會發現單級時間輪方式存在幾個缺陷:

  • 時間刻度太多會, 導致時間輪走到的多數刻度沒有任務執行,

    比如一個月就2個任務,我得移動720次,其中718次是無用功。

  • 時間刻度太多會導致儲存空間變大,利用率變低

    比如一個月就2個任務,我得需要大小是720的陣列,如果我的執行時間的粒度精確到秒,那就更恐怖了

所以,在實際應用中,一般單時間輪無法滿足需求。

例如我們需要秒級的精度,最大延遲可能是10天,那我們時間輪就要至少864000個格子,這就產生了如下幾個問題:

  1. 佔用儲存過大
  2. 利用率太低,比如我們只有1秒一個任務,而7天若干任務,那大部分時間,整個輪子都是在空轉。

如何解決單級時間輪的問題呢?

方案1:任務中新增round屬性

方案2:多級時間輪

任務中新增round屬性

這次,不增加時間輪的刻度了,時間輪的刻度還是24個,而是按照任務的時間間隔,給任務增加一個 round屬性。

一個round的單位,單表間隔為一輪。

假設:現在有三個任務需要執行:

  1. 任務一每週二上午九點。

  2. 任務二每週四上午九點。

  3. 任務三每個月12號上午九點。

比如現在是9月11號星期二上午10點,時間輪轉一圈是24小時,到任務一下次執行(下週二上午九點),

需要時間輪轉過6圈後,到第7圈的第9個刻度開始執行。

任務二下次執行第3圈的第9個刻度,任務三是第2圈的第9個刻度。

示意圖如下:

時間輪每移動到一個刻度時,遍歷任務列表,對每個task 進行分開處理:

  • 把round值-1,
  • 如果 round=0,則任務執行,從列表中移除。

這樣做能解決時間輪刻度範圍過大造成的空間浪費,但是卻帶來了另一個問題:

  • 時間輪每次都需要遍歷任務列表,耗時增加,當時間輪刻度粒度很小(秒級甚至毫秒級),
  • 任務列表又特別長時,這種遍歷的辦法是不可接受的。

當然,對於大多數場景,這種方法還是適用的。

有沒有既節省空間,又節省時間的辦法呢?

答案是有的,正如《Hashed and Hierarchical Timing Wheels》標題中提到的,有一種分層時間輪,可以解決做到既節省空間,又節省時間:

分層時間輪

分層時間輪是這樣一種思想:

  1. 針對時間複雜度的問題:

    不做遍歷計算round,凡是任務列表中的任務,都應該被執行的,直接全部取出來執行。

  2. 針對空間複雜度的問題:

    分層,每個時間粒度對應一個時間輪,多個時間輪之間進行級聯協作。

第一點很好理解,第二點有必要舉個例子來說明。

比如有三個任務:

  • 任務一 :間隔30s。
  • 任務二 :間隔1分鐘30s。
  • 任務三:間隔1小時1分鐘30s。

三個任務涉及到三個時間單位:秒、分鐘、小時。

按照分層時間輪來設計,我們可以設定三個時間輪:秒輪、分輪、小時輪。

時間刻度先得來到12號這一天,然後才需要關注其更細一級的時間單位:上午9點。

基於這個思想,我們可以設定三個時間輪:月輪、周輪、天輪。

  • 秒輪的時間刻度 tick duration 是秒。span 跨度為 一分鐘,60秒。
  • 分輪的時間刻度 tick duration 是分。span 跨度為 一小時,60分。
  • 小時輪的時間刻度是 tick duration 是小時。span 跨度為 一天,24小時。

初始新增任務時:

  • 任務一新增到秒輪上,
  • 任務二新增到分輪上
  • 任務三新增到時輪上。

三個時間輪以各自的時間刻度不停流轉。

當時輪移動到刻度2(第1小時)時,取出這個刻度下的任務三,丟到分輪上,分輪接管該任務。

當分輪移動到刻度2(第1分鐘)時,取出這個刻度下的任務二,丟到秒輪上,秒輪接管該任務。

當秒輪移動到刻度30(第30秒)時,取出這個刻度下的任務一,移除該任務,然後執行該任務。

整體的示意圖如下所示:

分層時間輪的時間複雜度分析

分層時間輪演算法是為了更高效的實現定時器而設計的一種資料格式,

定時器的核心需求

  1. 新增(初始化一個定時任務)
  2. 移除(過期任務)
  3. 任務到期檢測

定時器的實現方式,大概有那些呢?

方式一:基於小頂堆實現的PriorityQueue

內部的結構,按照到期時間,維護了**一個優先佇列 PriorityQueue **,

優先佇列的插入和刪除的時間複雜度是O(logn)

當資料量大的時候,頻繁的入堆出堆,總體效能有待考慮。

Timer、DelayQueue 和 ScheduledThreadPool

方式二:帶round屬性的單級時間輪

由於時間的跨度都比較長,單級時間輪一般會帶著round屬性。 沒有round屬性的單級時間輪,生產場景基本不用,這裡不做時間複雜度的考慮。

帶round屬性的單級時間輪的本質就是一個數組,它的時間跨度就是一個時間迴圈

同時,每一個槽位,會維護一個 任務連結串列,每一個任務帶著 round 數量

時間輪會以時間刻度 tick duration 間隔為單位,開始每一個最小時間間隔步進一個單位,然後檢查當前時間輪節點上是否有任務

  • 如果有任務,就直接執行
  • 沒有任務,就等待下一個時間間隔步進1,重複進行檢測

時間輪每移動到一個刻度時,遍歷任務列表,對每個task 進行分開處理:

  • 把round值 減去1,
  • 如果 round=0,則任務執行,從列表中移除。

時間複雜度:純粹的時間輪-新增O(1),移除O(1),檢測O(N)

總體而言,時間複雜度O(N)

缺點:時間複雜度高

方式三:分層時間輪實現定時器

本質就是多個時間輪共同一起作用,分時間層級!

以上述圖片為樣例,當時輪上有任務時,那麼就將該任務轉移到對應的分鐘時間輪上;

當分輪上有任務時,那麼就將該任務轉移到對應的秒鐘的秒輪上;

當秒輪上有任務時,那麼就將該任務移除;

時間複雜度:新增O(1),移除O(1),檢測O(1)

Caffeine 中的TimerWheel的使用

除了支援expireAfterAccess和expireAfterWrite之外(Guava Cache 也支援這兩個特性),Caffeine 還支援expireAfter。

因為expireAfterAccess和expireAfterWrite都只能是固定的過期時間,一般情況而已,這個已經夠用了。

但還是有些特殊場景,譬如記錄的過期時間,是需要根據某些條件而不一樣的,這就需要使用者自定義過期時間。

先看看expireAfter的用法

package com.github.benmanes.caffeine.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.concurrent.TimeUnit;

public class ExpireAfterDemo {
    static System.Logger logger = System.getLogger(ExpireAfterDemo.class.getName());

    public static void hello(String[] args) {
        System.out.println("args = " + args);
    }


    public static void main(String... args) throws Exception {
        Cache<String, String> cache =  Caffeine.newBuilder()
                //最大個數限制
                //最大容量1024個,超過會自動清理空間
                .maximumSize(1024)
                //初始化容量
                .initialCapacity(1)
                //訪問後過期(包括讀和寫)
                //5秒沒有讀寫自動刪除
//                .expireAfterAccess(5, TimeUnit.SECONDS)
                //寫後過期
//                .expireAfterWrite(2, TimeUnit.HOURS)
                //寫後自動非同步重新整理
//                .refreshAfterWrite(1, TimeUnit.HOURS)
                //記錄下快取的一些統計資料,例如命中率等
                .recordStats()
                .removalListener(((key, value, cause) -> {
                    //清理通知 key,value ==> 鍵值對   cause ==> 清理原因
                  System.out.println("removed key="+ key);
                }))
                .expireAfter(new Expiry<String, String>() {
                    //返回建立後的過期時間
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        System.out.println("1. expireAfterCreate key="+ key);
                        return 0;
                    }

                    //返回更新後的過期時間
                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        System.out.println("2. expireAfterUpdate key="+ key);
                        return 0;
                    }

                    //返回讀取後的過期時間
                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        System.out.println("3. expireAfterRead key="+ key);
                        return 0;
                    }
                })
                .recordStats()
                //使用CacheLoader建立一個LoadingCache
                .build(new CacheLoader<String, String>() {
                    //同步載入資料
                    @Nullable
                    @Override
                    public String load(@NonNull String key) throws Exception {
                        System.out.println("loading  key="+ key);
                        return "value_" + key;
                    }

                    //非同步載入資料
                    @Nullable
                    @Override
                    public String reload(@NonNull String key, @NonNull String oldValue) throws Exception {
                        System.out.println("reloading  key="+ key);
                        return "value_" + key;
                    }
                });

        //新增值
        cache.put("name", "瘋狂創客圈");
        cache.put("key", "一個高併發 研究社群");

        //獲取值
        @Nullable String value = cache.getIfPresent("name");
        System.out.println("value = " + value);
        //remove
        cache.invalidate("name");
        value = cache.getIfPresent("name");
        System.out.println("value = " + value);
    }

}

通過自定義過期時間,使得不同的 key 可以動態的得到不同的過期時間。

需要把expireAfterAccess和expireAfterWrite註釋了,因為這兩個特性不能跟expireAfter一起使用。只能做二選一,從原始碼中,也可以看到這點:

當使用了expireAfter特性後,Caffeine 會啟用一種叫“時間輪”的演算法來實現這個功能。

為什麼Caffeine 要用時間輪

好,重點來了,為什麼要用時間輪?

對expireAfterAccess和expireAfterWrite的實現是用一個AccessOrderDeque雙端佇列,它是 FIFO 的

因為它們的過期時間是固定的,從時間維度而言,而最早進入佇列的快取記錄Node節點,在頭部。

最新的節點、或者最晚進入的快取記錄Node節點,在尾部。

由於每次插入都在尾部,可以理解為這個佇列,暗含了一個規律:

元素是按照過期時間有序的

所以, 結論是:

在佇列頭的資料肯定是最早過期的,在佇列尾部的資料肯定是最晚過期的,

要處理過期資料時,只需要首先看看頭部是否過期,然後再挨個檢查就可以了。

但是,這個佇列有個要求:

各個節點的過期時間,要求是一樣的

如果過期時間不一樣的話,怎麼做過期時間檢查呢?

這就需要遍歷,還有一種方式是,對這個雙端佇列accessOrderQueue進行排序&插入,這個時間複雜度就不說O(1)了。

於是,Caffeine 用了一種更加高效、優雅的演算法-時間輪。

Caffeine 的五級時間輪的原始碼分析

重要屬性與方法

時間輪的具體實現為一個二維陣列,其陣列的具體位置存放的則為一個待執行節點的連結串列。

時間輪的二維陣列的第一個維度則是具體的時間間隔,分別是秒,分鐘,小時,天,4天,但但並沒有嚴格按照時間單位來區分單位,而是根據以上單位最接近的2的整數次冪作為時間間隔,因此在其第一個維度的時間間隔分別是1.07s,1.14m,1.22h,1.63d,6.5d。


  static final int[] BUCKETS = { 64, 64, 32, 4, 1 };
  static final long[] SPANS = {
      ceilingPowerOfTwo(TimeUnit.SECONDS.toNanos(1)), // 1.07s
      ceilingPowerOfTwo(TimeUnit.MINUTES.toNanos(1)), // 1.14m
      ceilingPowerOfTwo(TimeUnit.HOURS.toNanos(1)),   // 1.22h
      ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)),    // 1.63d
      BUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 6.5d
      BUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 6.5d
  };

當具體的時間事件要加入到時間輪時,將會根據該事件距離當前時間的最接近的單位,首先定位到二維陣列的第一個維度,具體後面會解釋。

為什麼要選擇最接近的2的整數而不是選擇具體的時間整數,是為了可以通過移位比較快速得到時間滾動在二維陣列的第一個維度的變動。

在時間輪中,具體記錄了以上時間間隔的偏移量,在時間輪中,將當前時間與上一次時間求差並不斷右移SHIFT的位數,便可以快速定位到時間的變動。


  //Long.numberOfTrailingZeros(value1) 返回最左側之後的0位數,  10100010000 》》》 4
  static final long[] SHIFT = {
      Long.numberOfTrailingZeros(SPANS[0]),
      Long.numberOfTrailingZeros(SPANS[1]),
      Long.numberOfTrailingZeros(SPANS[2]),
      Long.numberOfTrailingZeros(SPANS[3]),
      Long.numberOfTrailingZeros(SPANS[4]),
  };

  final Node<K, V>[][] wheel;

  long nanos;

用簡單的數字舉個例子,第一個維度分別為1s,10s,100s,1000s,

那麼具體的第一維存在四個槽位,分別存放10s以內的,10s到100s以內的,100s到1000s以內的,1000s以後的,當一個300s之後發生的時間事件進入後,首先得到差值300,依次比較,顯然300大於100小於1000,那麼這個300s以後發生的事件在時間輪上的二維陣列的第一個維度的第三個位置上。

其中1s則是用來標誌過期操作之間的最小的時間刻度,並沒有參與到時間事件的定位中,類比時間輪中的1.07s。

請參見視訊《第25章:穿透Caffeine 的架構和原始碼分析》

建構函式

初始化五級時間輪,每個時間輪,都是一個數組。


  @SuppressWarnings({"rawtypes", "unchecked"})
  TimerWheel() {
    wheel = new Node[BUCKETS.length][];
    for (int i = 0; i < wheel.length; i++) {
      wheel[i] = new Node[BUCKETS[i]];
      for (int j = 0; j < wheel[i].length; j++) {
        wheel[i][j] = new Sentinel<>();
      }
    }
    System.out.println("BUCKETS = " + Arrays.toString(BUCKETS));
    System.out.println("SPANS = " +Arrays.toString( SPANS));
    System.out.println("SHIFT = " + Arrays.toString(SHIFT));
    System.out.println("Long.toBinaryString(SPANS[0]) = " + Long.toBinaryString(SPANS[0]));
    System.out.println("wheel = " + wheel);
  }

請參見視訊《第25章:穿透Caffeine 的架構和原始碼分析》

時間步進

/**
   * Advances the timer and evicts entries that have expired.
   *
   * @param cache the instance that the entries belong to
   * @param currentTimeNanos the current time, in nanoseconds
   */
  public void advance(BoundedLocalCache<K, V> cache, long currentTimeNanos) {
    long previousTimeNanos = nanos;
    nanos = currentTimeNanos;

    // If wrapping then temporarily shift the clock for a positive comparison. We assume that the
    // advancements never exceed a total running time of Long.MAX_VALUE nanoseconds (292 years)
    // so that an overflow only occurs due to using an arbitrary origin time (System.nanoTime()).
    if ((previousTimeNanos < 0) && (currentTimeNanos > 0)) {
      previousTimeNanos += Long.MAX_VALUE;
      currentTimeNanos += Long.MAX_VALUE;
    }

    try {
      for (int i = 0; i < SHIFT.length; i++) {
        long previousTicks = (previousTimeNanos >>> SHIFT[i]);
        long currentTicks = (currentTimeNanos >>> SHIFT[i]);
        long delta = (currentTicks - previousTicks);
        if (delta <= 0L) {
          break;
        }
        expire(cache, i, previousTicks, delta);
      }
    } catch (Throwable t) {
      nanos = previousTimeNanos;
      throw t;
    }
  }

任務排程

首先找到對應的 時間輪,和時間輪裡邊的槽位

然後通過哨兵,插入任務

  * @param node the entry in the cache
   */
  public void schedule(Node<K, V> node) {
    Node<K, V> sentinel = findBucket(node.getVariableTime());
    link(sentinel, node);
  }

找到對應的 時間輪,和時間輪裡邊的槽位

 * @param time the time when the event fires
   * @return the sentinel at the head of the bucket
   */
  Node<K, V> findBucket(long time) {
    long duration = time - nanos;
    int length = wheel.length - 1;
    for (int i = 0; i < length; i++) {
      if (duration < SPANS[i + 1]) {
        long ticks = (time >>> SHIFT[i]);
        int index = (int) (ticks & (wheel[i].length - 1));
        return wheel[i][index];
      }
    }
    return wheel[length][0];
  }

請參見視訊《第25章:穿透Caffeine 的架構和原始碼分析》

Dubbo原始碼中的時間輪

後面會對照 Caffeine 的時間輪,分析Dubbo原始碼中的時間輪

未完待續

XXL-Job原始碼中的時間輪

後面會對照 Caffeine 的時間輪,分析XXL-Job原始碼中的時間輪

未完待續

參考文獻

https://www.likecs.com/show-204434429.html

http://www.cs.columbia.edu/~nahum/w6998/papers/sosp87-timing-wheels.pdf

https://blog.csdn.net/xinzhongtianxia/article/details/86221241

https://blog.csdn.net/m0_37039331/article/details/87401758

https://blog.csdn.net/qq924862077/article/details/112550085

https://baijiahao.baidu.com/s?id=1714290103234167995

https://www.cnblogs.com/smileIce/p/11156412.html

https://blog.csdn.net/bz120413/article/details/122107790

https://blog.csdn.net/Javaesandyou/article/details/123918852

https://blog.csdn.net/Javaesandyou/article/details/123918852

https://blog.csdn.net/FreeeLinux/article/details/54897192

https://blog.csdn.net/weixin_41605937/article/details/121972371

推薦閱讀: