面試:說說延遲任務的調度算法?

磊哥課程 2024-06-06 07:34:51

Netty 框架是以性能著稱的框架,因此在它的框架中使用了大量提升性能的機制,例如 Netty 用于實現延遲隊列的時間輪調度算法就是一個典型的例子。使用時間輪調度算法可以實現海量任務新增和取消任務的時間度爲 O(1),那麽什麽是時間輪調度算法呢?接下來我們一起來看。

1.延遲任務實現

在 Netty 中,我們需要使用 HashedWheelTimer 類來實現延遲任務,例如以下代碼:

public DelayTaskExample { public static void main(String[] args) { System.out.println("程序啓動時間:" + LocalDateTime.now()); NettyTask(); } private static void NettyTask() { // 創建延遲任務實例 HashedWheelTimer timer = new HashedWheelTimer(3, // 間隔時間 TimeUnit.SECONDS, // 間隔時間單位 100); // 時間輪中的槽數 // 創建任務 TimerTask task = new TimerTask() { @Override public void run(Timeout timeout) throws Exception { System.out.println("執行任務時間:" + LocalDateTime.now()); } }; // 將任務添加到延遲隊列中 timer.newTimeout(task, 0, TimeUnit.SECONDS); }}

以上程序的執行結果如下:

程序啓動時間:2024-06-04T10:16:23.033

執行任務時間:2024-06-04T10:16:26.118

從上述執行結果可以看出,我們使用 HashedWheelTimer 實現了延遲任務的執行。

2.時間輪調度算法

那麽問題來了,HashedWheelTimer 是如何實現延遲任務的?什麽是時間輪調度算法?

查看 HashedWheelTimer 類的源碼會發現,其實它是底層是通過時間輪調度算法來實現的,以下是 HashedWheelTimer 核心實現源碼(HashedWheelTimer 的創建源碼)如下:

private static HashedWheelBucket[] createWheel(int ticksPerWheel) { // 省略其他代碼 ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel); HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel]; for (int i = 0; i < wheel.length; i ++) { wheel[i] = new HashedWheelBucket(); } return wheel;}private static int normalizeTicksPerWheel(int ticksPerWheel) { int normalizedTicksPerWheel = 1; while (normalizedTicksPerWheel < ticksPerWheel) { normalizedTicksPerWheel <<= 1; } return normalizedTicksPerWheel;}private static final HashedWheelBucket { private HashedWheelTimeout head; private HashedWheelTimeout tail; // 省略其他代碼}

在 HashedWheelTimer 中,使用了 HashedWheelBucket 數組實現時間輪的概念,每個 HashedWheelBucket 表示時間輪中一個 slot(時間槽),HashedWheelBucket 內部是一個雙向鏈表結構,雙向鏈表的每個節點持有一個 HashedWheelTimeout 對象,HashedWheelTimeout 代表一個定時任務,每個 HashedWheelBucket 都包含雙向鏈表 head 和 tail 兩個 HashedWheelTimeout 節點,這樣就可以實現不同方向進行鏈表遍曆,如下圖所示:

時間輪算法的設計思想就來源于鍾表,如上圖所示,時間輪可以理解爲一種環形結構,像鍾表一樣被分爲多個 slot 槽位。每個 slot 代表一個時間段,每個 slot 中可以存放多個任務,使用的是鏈表結構保存該時間段到期的所有任務。時間輪通過一個時針隨著時間一個個 slot 轉動,並執行 slot 中的所有到期任務。

任務的添加是根據任務的到期時間進行取模,然後將任務分布到不同的 slot 中。如上圖所示,時間輪被劃分爲 8 個 slot,每個 slot 代表 1s,當前時針指向 2 時,假如現在需要調度一個 3s 後執行的任務,應該加入 2+3=5 的 slot 中;如果需要調度一個 12s 以後的任務,需要等待時針完整走完一圈 round 零 4 個 slot,需要放入第 (2+12)%8=6 個 slot。

那麽當時針走到第 6 個 slot 時,怎麽區分每個任務是否需要立即執行,還是需要等待下一圈 round,甚至更久時間之後執行呢?所以我們需要把 round 信息保存在任務中。例如圖中第 6 個 slot 的鏈表中包含 3 個任務,第一個任務 round=0,需要立即執行;第二個任務 round=1,需要等待 1*8=8s 後執行;第三個任務 round=2,需要等待 2*8=8s 後執行。所以當時針轉動到對應 slot 時,只執行 round=0 的任務,slot 中其余任務的 round 應當減 1,等待下一個 round 之後執行。

可以看出時間輪有點類似 HashMap,如果多個任務如果對應同一個 slot,處理沖突的方法采用的是拉鏈法。在任務數量比較多的場景下,適當增加時間輪的 slot 數量,可以減少時針轉動時遍曆的任務個數。

時間輪定時器最大的優勢就是,任務的新增和取消都是 O(1) 時間複雜度,而且只需要一個線程就可以驅動時間輪進行工作。

課後思考

Netty 中的時間輪調度算法有什麽缺點?

參考 & 鳴謝

《Netty核心原理剖析與RPC實踐》

本文已收錄到我的面試小站 [www.javacn.site](https://www.javacn.site),其中包含的內容有:Redis、JVM、並發、並發、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、設計模式、消息隊列等模塊。

0 阅读:9

磊哥課程

簡介:感謝大家的關注