TimingWheel[時間輪]介紹

Kafka的延遲操作是一個相對獨立的組件,他的主要功能是管理延遲操作,底層依賴於Kafka提供的時間輪實現。JDK本身提供的java.util.Timer也可以實現定時任務,但是如果系統請求量巨大,性能要求很高,他們底層所依賴的數據結構存取操作複雜度都是O(nlog(n))

爲了將時間複雜度降爲o(1),一般會使用其他方式的定時任務組件,比如zookeeper的時間桶方式處理session過期,netty也使用Hash

WheelTimer這種時間輪的實現。

Kafka時間輪的實現是TimingWheel,他是一個存儲定時任務的環形隊列(桶),底層使用數組實現,數組中每一個元素可以存放一個TimerTaskList對象

 

TimerTaskList是環形雙向鏈表,在其中鏈表項TimeTaskEntry封裝了真正的定時任務TimerTask。TimerTaskList使用expiration字段記錄了整個TimerTaskList的超時時間。TimeTaskEntry中的expirationMs字段記錄了超時時間戳,timerTask字段指向了對應的TimerTask任務.

TimerTask中的delayMs記錄了任務的延遲時間,timerTaskEntry記錄了TimerTaskEntry對象

 

TimingWheel提供了分層的概念,因爲年時間跨度比較大,數量很大,單層的時間輪會造成任務的round很大,單個格子鏈表很長。一般情況,第一層時間跨度是最小的,第二層時間跨度比較大。

如上圖所示:假設編號爲0的時間格或者桶保存着到期時間爲t,每一個tick的持續時間(tickDuration)爲20ms,在這個格子裏只能保存着到期時間爲[t~t+20]ms的任務,任務到底放在哪一個時間格或者桶裏面根據不同的場景可以有不同的算法,假設時間輪的時間格有n個,到期時間爲m(ms),那麼計算公式m%n = 所在的時間格或者桶,比如n=10,m=34ms,那麼他所在桶或者時間格是4

 

當任務到期時間超出了當前時間所表示的時間範圍時,就會嘗試加到上一層時間輪,如下圖所示:

其中第一層時間輪每個時間格是1ms,整個時間輪跨度是20ms,指針當前時間表示的時間是currentTime,則該時間輪跨度爲currentTime

~currentTime+20,只有時間在這段範圍內任務才能添加到該層時間輪等待到期。到期時間超出[currentTime~currentTime+20]這個時間範圍的任務會嘗試添加到上級時間輪中,通過逐層向上級嘗試最終找到合適的時間輪層級

 

整個時間輪表示的時間跨度是不變的,隨着指針的不斷後移,當前時間輪能處理的時間段也在不斷後移,新來的TimerTaskEntry會複用原來的已經到期的TimerTaskList,如下圖所示,第一層時間輪跨度始終爲20ms,指針表示的時間在不段後移。當指針指向0是時間格的時候,假設currentTime = 100,指向第三個時間格,此時指針表示的時間爲當前時間104ms,整個時間輪表示的時間段是[104~ 124],但是該時間輪的時間跨度依然是20ms。此時間輪中編號爲2的時間格表示的時間不再是102~103,而是123~124


假設現在有一個任務在445ms後執行,默認情況下,各個層級的時間輪的時間格個數爲20,第一層時間輪每一個時間格跨度爲1ms,整個時間輪跨度爲20ms,跨度不夠。第二層時間輪每一個時間格跨度爲20ms,整個時間輪跨度爲400ms,跨度依然不夠,第三層時間輪每一個時間格跨度爲400ms,整個時間輪跨度爲8000ms,現在跨度夠了,此任務就放在第三層時間輪的第一個時間格對應的TimerTaskList,等待被執行,此TimerTaskList到期時間是400ms,隨着時間的流逝,當此TimerTaskList到期時,距離該任務到期時間還有45ms,不能執行該任務,我們將重新提交到時間輪,此時第一層時間輪跨度依然不夠,不能執行任務,第二層時間輪時間格跨度爲20,整個世間輪跨度爲400,跨度足夠,放在第三個時間格等待執行,如此往復幾次,高層時間輪最終會慢慢移動到低層時間輪上,最終任務到期執行。

 

一 重要屬性

buckets : Array.tabulate[TimerTaskList] 類型,其每一個項都對應時間輪中一個時間格,用於保存TimerTaskList的數組

tickMs:Long 當前時間輪中一個時間格表示的時間跨度

wheelSize: Int 當前時間輪的大小也就是總的時間格數量

taskCounter:AtomicInteger 各層級時間輪中任務的總數

startMs:Long 當期時間輪的創建時間

queue:DelayQueue[TimerTaskList] 整個層級的時間輪公用一個任務隊列,其元素類型是TimerTaskList

currentTime:時間輪的指針,將整個時間輪劃分爲到期部分和未到期部分。在初始化的時候,currentTime被修剪成tickMs的倍數startMs - (startMs % tickMs)

interval:Long 當前時間輪的時間跨度即tickMs * wheelSize,當前時間輪只能處理時間範圍在currentTime~currentTime+tickMs*WheelSize之間的定時任務,超過這個範圍則需要添加任務到上層時間輪

overflowWheel: TimingWheel 上層時間輪的引用

 

二 核心方法

2.1 addOverflowWheel 主要用於創建上層時間輪

private[this] def addOverflowWheel(): Unit = {
  synchronized {
    if (overflowWheel == null) {
      overflowWheel = new TimingWheel(
        tickMs = interval, // 上層時間輪的時間格跨度等於下一層時間輪的跨度
        wheelSize = wheelSize,// 大小不變
        startMs = currentTime,// 初始化當前時間輪的創建時間
        taskCounter= taskCounter,//時間輪中任務的總數
        queue
      )
    }
  }
}

 

2.2 add 向時間輪中添加定時任務,同時也會檢測添加的任務是否已經到期

def add(timerTaskEntry: TimerTaskEntry): Boolean = {
  // 獲取定時任務的超時時間戳
  val expiration = timerTaskEntry.expirationMs
  // 如果任務已經被取消
  if (timerTaskEntry.cancelled) {
    false // 返回添加失敗
  } else if (expiration < currentTime + tickMs) { // 如果時間指針現在指向的時間+時間格跨度 > 需要添加的定時任務的超時時間戳表示已經到期
    // 舉個例子:currentTime=102,時間格跨度爲10ms,那麼假設添加的任務超時時間戳爲105 < 102+10
    false // 返回添加失敗
  } else if (expiration < currentTime + interval) {// 如果時間指針現在指向的時間+當前時間輪跨度 > 需要添加的定時任務的超時時間戳表示沒有到期
    // 然後把當前任務放進循環數組裏面
    // 得到任務到期時間戳/時間格跨度的餘數
    val virtualId = expiration / tickMs
    // 獲取放在哪一個桶裏 (到期時間戳/時間格跨度)%時間輪跨度
    val bucket = buckets((virtualId % wheelSize.toLong).toInt)
    bucket.add(timerTaskEntry)

    // 設置時間格的到期時間
    if (bucket.setExpiration(virtualId * tickMs)) {
      /*
       * 整個時間輪表示的跨度是不變的,隨着指針的後移,當前時間輪能夠處理的時間段也在不段後移,新的TimerTaskEntry會複用原來的已經清理過的
       * TimerTaskList,此時你需要重新設置TimerTaskList的到期時間,並將桶重新入隊
       */
      queue.offer(bucket)
    }
    true
  } else { // 如果超出了時間的跨度範圍,則將其添加到上層時間輪來處理
    if (overflowWheel == null) addOverflowWheel()
    overflowWheel.add(timerTaskEntry)
  }
}
2.3 advanceClock 嘗試推進當前時間輪的指針,同時也會嘗試推進上層時間輪的指針,隨着當前時間輪的不斷推進,上層時間輪指針早晚會被推進成功

def advanceClock(timeMs: Long): Unit = {
  // 嘗試移動指針currentTime
  if (timeMs >= currentTime + tickMs) {
    currentTime = timeMs - (timeMs % tickMs)

    // 嘗試禿頂上層時間輪指針currentTime
    if (overflowWheel != null) overflowWheel.advanceClock(currentTime)
  }
}
————————————————
版權聲明:本文爲CSDN博主「happy19870612」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/zhanglh046/article/details/72833172

發佈了172 篇原創文章 · 獲贊 157 · 訪問量 255萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章