面試官:你給我說一下什麼是時間輪吧?

why技術發表於2021-11-16

你好呀,我是歪歪。

今天我帶大家來卷一下時間輪吧,這個玩意其實還是挺實用的。

常見於各種框架之中,偶現於面試環節,理解起來稍微有點難度,但是知道原理之後也就覺得:

大多數人談到時間輪的時候都會從 netty 開始聊。

我就不一樣了,我想從 Dubbo 裡面開始講,畢竟我第一次接觸到時間輪其實是在 Dubbo 裡面,當時就驚豔到我了。

而且,Dubbo 的時間輪也是從 Netty 的原始碼裡面拿出來的,基本一模一樣。

時間輪在 Dubbo 裡面有好幾次使用,比如心跳包的傳送、請求呼叫超時時間的檢測、還有叢集容錯策略裡面。

我就從 Dubbo 裡面這個類說起吧:

org.apache.dubbo.rpc.cluster.support.FailbackClusterInvoker

Failback,屬於叢集容錯策略的一種:

你不瞭解 Dubbo 也沒有關係,你只需要知官網上是這樣介紹它的就行了:

我想突出的點在於“定時重發”這四個字。

我們先不去看原始碼,提到定時重發的時候,你想到了什麼東西?

是不是想到了定時任務?

那麼怎麼去實現定時任務呢?

大家一般都能想到 JDK 裡面提供的 ScheduledExecutorService 和 Timer 這兩個類。

Timer 就不多說了,效能不夠高,現在已經不建議使用這個東西。

ScheduledExecutorService 用的還是相對比較多的,它主要有三個型別的方法:

簡單說一下 scheduleAtFixedRate 和 scheduleWithFixedDelay 這兩個方法。

ScheduleAtFixedRate,是每次執行時間為上一次任務開始起向後推一個時間間隔。

ScheduleWithFixedDelay,是每次執行時間為上一次任務結束起向後推一個時間間隔。

前者強調的是上一個任務的開始時間,後者強調的是上一個任務的結束時間。

你也可以理解為 ScheduleAtFixedRate 是基於固定時間間隔進行任務排程,而 ScheduleWithFixedDelay 取決於每次任務執行的時間長短,是基於不固定時間間隔進行任務排程。

所以,如果是我們要基於 ScheduledExecutorService 來實現前面說的定時重發功能,我覺得是用 ScheduleWithFixedDelay 好一點,含義為前一次重試完成後才應該隔一段時間進行下一次重試。

讓整個重試功能序列化起來。

那麼 Dubbo 到底是怎麼實現這個定時重試的需求的呢?

擼原始碼啊,原始碼之下無祕密。

準備發車。

擼原始碼

有的同學看到這裡可能著急了:不是說講時間輪嗎,怎麼又開始擼原始碼了呀?

你別猴急呀,我這不得循序漸進嘛。

我先帶你手撕一波 Dubbo 的原始碼,讓你知道原始碼這樣寫的問題是啥,然後我再說解決方案嘛。

再說了,我直接,啪的一下,把解決方案扔你臉上,你也接受不了啊。

我喜歡溫柔一點的教學方式。

好了,先看下面的原始碼。

這幾行程式碼你要是沒看明白沒有關係,你主要關注 catch 裡面的邏輯。

我把程式碼和官網上的介紹幫你對應了一下。

意思就是呼叫失敗了,還有一個 addFailed 來兜底。

addFailed 是幹了啥事呢?

乾的就是“定時重發”這事:

org.apache.dubbo.rpc.cluster.support.FailbackClusterInvoker#addFailed

這個方法就可以回答前面我們提出的問題:Dubbo 叢集容錯裡面,到底是怎麼實現這個定時重試的需求的呢?

從標號為 ① 的地方可以知道,用的就是 ScheduledExecutorService,具體一點就是用的 scheduleWithFixedDelay 方法。

再具體一點就是如果叢集容錯採用的是 failback 策略,那麼在請求呼叫失敗的 RETRY_FAILED_PERIOD 秒之後,以每隔 RETRY_FAILED_PERIOD 秒一次的頻率發起重試,直到重試成功。

RETRY_FAILED_PERIOD 是多少呢?

看第 52 行,它是 5 秒。

另外,你可以在前面 addFailed 方法中看到標號為 ③ 的地方,是在往 failed 裡面 put 東西。

failed 又是一個什麼東西呢?

看前面的 61 行,是一個 ConcurrentHashMap。

標號為 ③ 的地方,往 failed put 的 key 就是這一次需要重試的請求,value 是處理這一次請求對應的服務端。

failed 這個 map 是什麼時候用呢?

請看標號為 ② 的 retryFailed 方法:

在這個方法裡面會去遍歷 failed 這個 map,全部拿出來再次呼叫一遍。

如果成功了就呼叫 remove 方法移除這個請求,沒有成功的會丟擲異常,列印日誌,然後等待下次再次重試。

到這裡我們就算是揭開了 Dubbo 的 FailbackClusterInvoker 類的神祕面紗。

面紗之下,隱藏的就是一個 map 加 ScheduledExecutorService。

感覺好像也沒啥難的啊,很常規的解決方案嘛,我也能想到啊。

於是你緩緩的在螢幕上打出一個:

但是,朋友們,抓好坐穩,要“但是”了,要轉彎了。

這裡面其實是有問題的,最直觀的就是這個 map,沒有限制大小,由於沒有限制大小,那麼在一些高併發的場景下,是有可能出現記憶體溢位的。

好,那麼問題來了,怎麼防止記憶體溢位呢?

很簡單,首先我們可以限制 map 的大小,對吧。

比如限制它的容量為 1000。

滿了之後,怎麼辦呢?

可以搞一個淘汰策略嘛,先進先出(FIFO),或者後進先出(LIFO)。

然後也不能一直重試,如果重試超過了一定次數應該被幹掉才對。

上面說的記憶體溢位和解決方案,都不是我亂說的。

我都是有證據的,因為我是從 FailbackClusterInvoker 這個類的提交記錄上看到了它的演進過程的,前面截圖的程式碼也是優化之前版本的程式碼,並不是最新的程式碼:

這一次提交,提到了一個編號叫 2425 的 issue。

https://github.com/apache/dubbo/issues/2425

這裡面提到的問題和解決方案,就是我前面說的事情。

終於,鋪墊完成,關於時間輪的故事要正式開始了。

時間輪原理

有的朋友又開始猴急了。

要我趕緊上時間輪的原始碼。

你彆著急啊,我直接給你講原始碼,你肯定會看懵逼的。

所以我決定,先給你畫圖,看懂原理。

給大家畫一下時間輪的基本樣子,理解了時間輪的工作原理,下面的原始碼解析理解起來也就相對輕鬆一點了。

首先時間輪最基本的結構其實就是一個陣列,比如下面這個長度為 8 的陣列:

怎麼變成一個輪呢?

首尾相接就可以了:

假如每個元素代表一秒鐘,那麼這個陣列一圈能表達的時間就是 8 秒,就是這樣的:

注意我前面強調的是一圈,為 8 秒。

那麼 2 圈就是 16 秒, 3 圈就是 24 秒,100 圈就是 800 秒。

這個能理解吧?

我再給你配個圖:

雖然陣列長度只有 8,但是它可以在上疊加一圈又一圈,那麼能表示的資料就多了。

比如我把上面的圖的前三圈改成這樣畫:

希望你能看明白,看不明白也沒有關係,我主要是要你知道這裡面有一個“第幾圈”的概念。

好了,我現在把前面的這個陣列美化一下,從視覺上也把它變成一個輪子。

輪子怎麼說?

輪子的英文是 wheel,所以我們現在有了一個叫做 wheel 的陣列:

然後,把前面的資料給填進去大概是長這樣的。

為了方便示意,我只填了下標為 0 和 3 的位置,其他地方也是一個意思:

那麼問題就來了。假設這個時候我有一個需要在 800 秒之後執行的任務,應該是怎麼樣的呢?

800 mod 8 =0,說明應該掛在下標為 0 的地方:

假設又來一個 400 秒之後需要執行的任務呢?

同樣的道理,繼續往後追加即可:

不要誤以為下標對應的連結串列中的圈數必須按照從小到大的順序來,這個是沒有必要的。

好,現在又來一個 403 秒後需要執行的任務,應該掛在哪兒?

403 mod 8 = 3,那麼就是這樣的:

我為什麼要不厭其煩的給你說怎麼計算,怎麼掛到對應的下標中去呢?

因為我還需要引出一個東西:待分配任務的佇列。

上面畫 800 秒、 400 秒和 403 秒的任務的時候,我還省略了一步。

其實應該是這樣的:

任務並不是實時掛到時間輪上去的,而是先放到一個待分配的佇列中,等到特定的時間再把待分配佇列中的任務掛到時間輪上去。

具體是什麼時候呢?

下面講原始碼的時候再說。

其實除了待分配佇列外,還有一個任務取消的佇列。

因為放入到時間輪的任務是可以被取消的。

比如在 Dubbo 裡面,檢驗呼叫是否超時也用的是時間輪機制。

假設一個呼叫的超時時間是 5s,5s 之後需要觸發任務,丟擲超時異常。

但是如果請求在 2s 的時候就收到了響應,沒有超時,那麼這個任務是需要被取消的。

對應的原始碼就是這塊,看不明白沒關係,看一眼就行了,我只是為了證明我沒有騙你:

org.apache.dubbo.remoting.exchange.support.DefaultFuture#received

原理畫圖出來大概就是這樣,然後我還差一張圖。

把原始碼裡面的欄位的名稱給你對應到上面的圖中去。

主要把這幾個物件給你對應上,後面看原始碼就不會太吃力了:

對應起來是這樣的:

注意左上角的“worker的工作範圍”把整個時間輪包裹了起來,後面看原始碼的時候你會發現其實整個時間輪的核心邏輯裡面沒有執行緒安全的問題,因為 worker 這個單執行緒把所有的活都幹完了。

最後,再提一嘴:比如在前面 FailbackClusterInvoker 的場景下,時間輪觸發了重試的任務,但是還是失敗了,怎麼辦呢?

很簡單,再次把任務放進去就行了,所以你看原始碼裡面,有一個叫做 rePut 的方法,乾的就是這事:

org.apache.dubbo.rpc.cluster.support.FailbackClusterInvoker.RetryTimerTask#run

這裡的含義就是如果重試出現異常,且沒有超過指定重試次數,那麼就可以再次把任務仍回到時間輪裡面。

等等,我這裡知道“重試次數”之後,還能幹什麼事兒呢?

比如如果你對接過微信支付,它的回撥通知有這樣的一個時間間隔:

我知道當前重試的次數,那麼我就可以在第 5 次重試的時候把時間設定為 10 分鐘,扔到時間輪裡面去。

時間輪就可以實現上面的需求。

當然了,MQ 的延遲佇列也可以,但是不是本文的討論範圍。

但是用時間輪來做上面這個需求還有一個問題:那就是任務在記憶體中,如果服務掛了就沒有了,這是一個需要注意的地方。

除了 FailbackClusterInvoker 外,其實我覺得時間輪更合適的地方是做心跳。

這可太合適了, Dubbo 的心跳就是用的時間輪來做。

org.apache.dubbo.remoting.exchange.support.header.HeartbeatTimerTask#doTask

從上圖可以看到,doTask 方法就是傳送心跳包,每次傳送完成之後呼叫 reput 方法,然後再次把傳送心跳包的任務仍回給時間輪。

好了,不再擴充套件應用場景了。

接下來,進入原始碼分析,跟上節奏,不要亂,大家都能學。

開卷!

時間輪原始碼

前面把原理理解到位了,接下來就可以看一下我們的原始碼了。

先說明一下,為了方便我截圖,下面的部分截圖我是移動了原始碼的位置,所以可能和你看原始碼的時候有點不一樣。

我們再次審視 Dubbo 的 FailbackClusterInvoker 類中關於時間輪的用法。

首先 failTimer 這個物件,是一個很眼熟的雙重檢查的單例模式:

這裡初始化的 failTimer 就是 HashedWheelTimer 物件關鍵的邏輯是呼叫了它的構造方法。

所以,我們先從它的構造方法入手,開始撕它。

先說一下它的幾個入參分別是幹啥的:

  • threadFactory:執行緒工廠,可以指定執行緒的名稱和是否是守護程式。
  • tickDuration:兩個 tick 之間的時間間隔。
  • unit:tickDuration 的時間單位。
  • ticksPerWheel:時間輪裡面的 tick 的個數。
  • maxPendingTimeouts:時間輪中最大等待任務的個數。

所以,Dubbo 這個時間輪的含義就是這樣的:

建立一個執行緒名稱為 failback-cluster-timer 的守護執行緒,每隔一秒執行一次任務。這個時間輪的大小為 32,最大的等待處理任務個數是 failbackTasks,這個值是可以配置的,預設值是 100。

但是很多其他的使用場景下,比如 Dubbo 檢查呼叫是否超時,就沒有送 maxPendingTimeouts 這個值:

org.apache.dubbo.remoting.exchange.support.DefaultFuture#TIME_OUT_TIMER

它甚至連 ticksPerWheel 都沒有上送。

其實這兩個引數都是有預設值的。ticksPerWheel 預設為 512。maxPendingTimeouts 預設為 -1,含義為對等待處理的任務個數不限制:

好了,現在我們整體看一下這個時間輪的構造方法,每一行的作用我都寫上了註釋:

有幾個地方,我也單獨拿出來給你說一下。

比如 createWheel 這個方法,如果你八股文背的熟悉的話,你就知道這裡和 HashMap 裡面確認容量的核心程式碼是一樣一樣的。

這也是我在原始碼註釋裡面提到的,時間輪裡面陣列的大小必須是 2 的 n 次方。

為什麼,你問我為什麼?

別問,問就是為了後面做位運算,操作騷,速度快,逼格高。

我相信下面的這一個程式碼片段不需要我來解釋了,你要是不理解,就再去翻一番 HashMap 的八股文:

但是這一行程式碼我還是可以多說一句的 mask = wheel.length - 1

因為我們已經知道 wheel.length 是 2 的 n 次方。

那麼假設我們的定時任務的延遲執行時間是 x,那麼它應該在時間輪的哪個格子裡面呢?

是不是應該用 x 對長度取餘,也就是這樣計算: x % wheel.length。

但是,取餘操作的效率其實不算高。

那麼怎麼能讓這個操作快起來呢?

就是 wheel.length - 1。

wheel.length 是 2 的 n 次方,減一之後它的二級制的低位全部都是 1,舉個例子就是這樣式兒的:

所以 x % wheel.length = x & (wheel.length - 1)。

在原始碼裡面 mask =wheel.length - 1。

那麼 mask 在哪用的呢?

其中的一個地方就是在 Worker 類的 run 方法裡面:

org.apache.dubbo.common.timer.HashedWheelTimer.Worker

這裡計算出來的 idx 就是當前需要處理的陣列的下標。

我這裡只是告訴你 mask 確實是參與了 & 位運算,所以你看不懂這塊的程式碼也沒有關係,因為我還沒講到這裡來。

所以沒跟上的同學不要慌,我們接著往下看。

前面我們已經有一個時間輪了,那麼怎麼呼叫這個時間呢?

其實就是呼叫它的 newTimeout 方法:

這個方法有三個入參:

含義很明確,即指定任務(task)在指定時間(delay,unit)之後開始觸發。

接下來解讀一下 newTimeout 方法:

裡面最關鍵的程式碼是 start 方法,我帶大家看一下到底是在幹啥:

分成上下兩部分講。

上面其實就是維護或者判斷當前 HashedWheelTimer 的狀態,從原始碼中我們知道狀態有三個取值:

  • 0:初始化
  • 1:已啟動
  • 2:已關閉

如果是初始化,那麼通過一個 cas 操作,把狀態更新為已啟動,並執行 workerThread.start() 操作,啟動 worker 執行緒。

下面這個部分就稍微有一點點費解了。

如果 startTime 等於 0,即沒有被初始化的話,就呼叫 CountDownLatch 的 await 等待一下下。

而且這個 await 還是在主執行緒上的 await,主執行緒在這裡等著 startTime 被初始化,這是個什麼邏輯呢?

首先,我們要找一下 startTime 是在哪兒被初始化的。

就是在 Worker 的 run 方法裡面,而這個方法就是在前面 workerThread.start() 的時候觸發的:

org.apache.dubbo.common.timer.HashedWheelTimer.Worker

可以看到,對 startTime 初始化完成後,還判斷了是否等於 0。也就是說 System.nanoTime() 方法是有可能返回為 0,一個小細節,如果你去要深究一下的話,也是很有趣的,我這裡就不展開了。

startTime 初始化完成之後,立馬執行了 startTimeInitialized.countDown() 操作。

這不就和這裡呼應起來了嗎?

主執行緒不馬上就可以跑起來了嗎?

那麼問題就來了,這裡大費周章的搞一個 startTime 初始化,搞不到主執行緒還不能繼續往下執行是幹啥呢?

當然是有用啦,回到 newTimeout 方法接著往下看:

我們分析一下上面這個等式哈。

首先 System.nanoTime() 是程式碼執行到這個地方的實時時間。

因為 delay 是一個固定值,所以 unit.toNanos(delay) 也是一個固定值。

那麼 System.nanoTime()+unit.toNanos(delay) 就是這個任務需要被觸發的納秒數。

舉個例子。

假設 System.nanoTime() = 1000,unit.toNanos(delay)=100。

那麼這個任務被觸發的時間點就是 1000+100=1100。

這個能跟上吧?

那麼為什麼要減去 startTime 呢?

startTime 我們前面分析了,其實初始化的時候也是 System.nanoTime(),初始化完成後就是一個固定值了。

那豈不是 System.nanoTime()-startTime 幾乎趨近於 0?

這個等式 System.nanoTime()+unit.toNanos(delay)-startTime 的意義是什麼呢?

是的,這就是我當時看原始碼的一個疑問。

但是後面我分析出來,其實整個等式裡面只有 System.nanoTime() 是一個變數。

第一次計算的時候 System.nanoTime()-startTime 確實趨近於 0,但是當第二次觸發的時候,即第二個任務來的時候,計算它的 deadline 的時候,System.nanoTime() 可是遠大於 startTime 這個固定值的。

所以,第二次任務的執行時間點應該是當前時間加上指定的延遲時間減去 worker 執行緒的啟動時間,後面的時間以此類推。

前面 newTimeout 方法就分析完了,也就是主執行緒在這個地方就執行完時間輪相關的邏輯了。

接下來該分析什麼呢?

肯定是該輪到時間輪的 worker 執行緒上場發揮了啊。

worker 執行緒的邏輯都在 run 方法裡面。

而核心邏輯就在一個 do-while 裡面:

迴圈結束的條件是當前時間輪的狀態不是啟動狀態。

也就是說,只要時間輪沒有被呼叫 stop 邏輯,這個執行緒會一直在執行。

接下來我們逐行看一下迴圈裡面的邏輯,這部分邏輯就是時間輪的核心邏輯。

首先是 final long deadline = waitForNextTick() 這一行,裡面就很有故事:

首先你看這個方法名你就知道它是幹啥的了。

是在這裡面等待,直到下一個時刻的到來。

所以方法進來第一行就是計算下一個時刻的納秒值是啥。

接著看 for 迴圈裡面,前面部分都看的比較懵逼,只有標號為 ③ 的地方好理解的多,就是讓當前執行緒睡眠指定時間。

所以前面的部分就是在算這個指定時間是什麼。

怎麼算的呢?

標號為 ① 的地方,前面部分還能看懂,

deadline - currentTime 算出來的就是還需要多長時間才會到下一個時間刻度。

後面直接就看不懂了。

裡面的 1000000 好理解,單位是納秒,換算一下就是 1 毫秒。

這個 999999 是啥玩意?

其實這裡的 999999 是為了讓算出來的值多 1 毫秒。

比如,deadline - currentTime 算出來是 1000123 納秒,那麼 1000123/1000000=1ms。

但是(1000123+999999)/1000000=2ms。

也就是說要讓下面標號為 ③ 的地方,多睡 1ms。

這是為什麼呢?

我也不知道,所以我先暫時不管了,留個坑嘛,問題不大,接著往下寫。

下面就到了標號為 ② 的地方,看起來是對 windows 作業系統進行了特殊的處理,要把 sleepTimeMs 換算為 10 的倍數。

為啥?

這裡我就得批評一下 Dubbo 了,把 Netty 的實現拿過來了,還把關鍵資訊給隱藏了,這不合適吧。

這地方在 Netty 的原始碼中是這樣的:

這裡很清晰的指了個路:

https://github.com/netty/netty/issues/356

而順著這條路,一路往下跟,會找到這樣一個地方:

https://www.javamex.com/tutorials/threads/sleep_issues.shtml

沒想到還有意外收穫。

第一個劃線的地方大概意思是說當執行緒呼叫 Thread.sleep 方法的時候,JVM 會進行一個特殊的呼叫,將中斷週期設定為 1ms。

因為 Thread.sleep 方法的實現是依託於作業系統提供的中斷檢查,也就是作業系統會在每一箇中斷的時候去檢查是否有執行緒需要喚醒並且提供 CPU 資源。所以我覺得前面多睡 1ms 的原因就可以用這個原因來解釋了。

前面留的坑,這麼快就填上了,舒服。

而第二個劃線的地方說的是,如果是 windows 的話,中斷週期可能是 10ms 或者 15ms,具體和硬體相關。

所以,如果是 windows 的話,需要把睡眠時間調整為 10 的倍數。

一個沒啥卵用的知識,送給你。

前面幾個問題了解清楚了,waitForNextTick 方法也就理解到位了,它乾的事兒就是等,等一個時間刻度的時間,等一個 tick 長度的時間。

等到了之後呢?

就來到了這一行程式碼 int idx = (int) (tick & mask)

我們前面分析過,計算當前時間對應的下標,位運算,操作騷,速度快,逼格高,不多說。

然後程式碼執行到這個方法 processCancelledTasks()

看方法名稱就知道了,是處理被取消的任務的佇列:

邏輯很簡單,一目瞭然,就是把 cancelledTimeouts 佇列給清空。

這裡是在 remove,在清理。

那麼哪裡在 add,在新增呢?

就是在下面這個方法中:

org.apache.dubbo.common.timer.HashedWheelTimer.HashedWheelTimeout#cancel

如果呼叫了 HashedWheelTimeout 的 cancel 方法,那麼這個任務就算是被取消了。

前面畫圖的時候就提到了這個方法,邏輯也很清晰,所以不多解釋了。

但是你注意我畫了下劃線的地方:MpscLinkedQueue。

這是個啥?

這是一個非常牛逼的無鎖佇列。

但是 Dubbo 這裡的 cancelledTimeouts 佇列的資料結構明明用的是 LinkedBlockingQueue 呀?

怎麼回事呢?

因為這裡的註釋是 Netty 裡面的,Netty 裡面用的是 MpscLinkedQueue。

你看我給你對比一下 Netty 和 Dubbo 這裡的區別:

所以這裡的註解是有誤導的,你有時間的話可以給 Dubbo 提給 pr 修改一下。

又拿捏了一個小細節。

好了,我們接著往下卷,來到了這行程式碼 HashedWheelBucket bucket=wheel[idx]

一目瞭然,沒啥說的。

從時間輪裡面獲取指定下標的 bucket。

主要看看它下面的這一行程式碼 transferTimeoutsToBuckets()

我還是每一行都加上註釋:

所以這個方法的核心邏輯就是把等待分配的任務都發配到指定的 bucket 上去。

這裡也就回答了我畫圖的時候留下的一個問題:什麼時候把等待分配佇列裡面的任務掛到時間輪上去呢?

就是這個時候。

接下來分析 bucket.expireTimeouts(deadline) 這一行程式碼。

你看這個方法的呼叫方就是 bucket,它代表的含義就是準備開始處理這個 bucket 裡面的這個連結串列中的任務了:

最後,還有一行程式碼 tick++

表示當前這個 tick 已經處理完成了,開始準備下一個時間刻度。

關鍵程式碼就分析完了。

一遍看不懂就多看一遍,但是我建議你自己也對照著原始碼一起看,很快就能搞懂。

相信以後面試官問到時間輪的時候你可以和他戰鬥上一個回合了。

為什麼是一個回合呢?

因為得你回答完這個時間輪後,一般來說,面試官會追問一個:

嗯,說的很不錯,那你再介紹一下層級時間輪吧?

當時你就懵逼了:什麼,層級時間輪是什麼鬼,歪歪沒寫啊?

是的,怪我,我沒寫,下次,下次一定。

但是我可以給你指條路,去看看 kafka 對於時間輪的優化。你會看的鼓起掌來。

幾個相關的 issues

最後,關於 Dubbo 時間輪,在 issues 裡面有一個討論:

https://github.com/apache/dubbo/issues/3324

大家有興趣的可以去看看。

其中提到了一個有意思的問題:

Netty 在 3.x 中有大量使用 HashedWheelTimer,但是在 4.1 中,我們可以發現,Netty 保留了 HashedWheelTimer,但在其原始碼中並未使用它,而是選擇了 ScheduledThreadPoolExecutor,不知道它的用意是什麼。

這個問題得到了 Netty 的維護者的親自答:

https://github.com/netty/netty/issues/8774

他的意思是時間輪其實沒有任何毛病,我沒有用只是因為我們希望與通道的EventLoop位於同一執行緒上。

在 Netty 裡面,有個老哥發現時間輪並沒有用上了,甚至想把它給幹掉:

我尋思這屬於工具類啊,你留著唄,總是會有用的。

另外,前面的 issue 還提到了另外一個問題:

https://github.com/apache/dubbo/issues/1371

這也是 Dubbo 引入時間輪之後進行的優化。

帶你看一眼,上面是優化之後的,下面是之前的寫法:

在之前的寫法中,就是後臺起一個執行緒,然後搞個死迴圈,一遍遍的去掃整個集合:

這種方案也能實現需求,但是和時間輪的寫法比起來,高下立判。

操作騷,速度快,逼格高。

最後說一句

好了,看到了這裡了,轉發、在看、點贊隨便安排一個吧,要是你都安排上我也不介意。寫文章很累的,需要一點正反饋。

給各位讀者朋友們磕一個了:

相關文章