管理大量定時任務,如果高效觸發超時?


1. 背景

很多時候,業務有定時任務或定時超時的需求,當任務量很大時,可能需要維護大量的timer,或者進行低效的掃描。

例如:對每個用戶會維護一個APP到服務器的TCP連接,用來實時收發信息,對這個TCP連接,如果連續30s沒有請求包,服務端就要將這個連接斷開。

一般說怎么實現這類需求呢?

2. 一般思路

2.1 輪詢掃描法

(1)用一個Map<uid, last_packet_time>來記錄每一個uid最近一次請求時間last_packet_time;

(2)當某個用戶uid有請求包來到,實時更新這個Map;

(3)啟動一個timer,當Map中不為空時,輪詢掃描這個Map,檢查每個uid的last_packet_time是否超過30s,如果超過則進行超時處理

2.2 多timer觸發法

(1)用一個Map<uid, last_packet_time>來記錄每一個uid最近一次請求時間last_packet_time;

(2)當某個用戶uid有請求包來到,實時更新這個Map,並同時對這個uid請求包啟動一個timer,30s之后觸發;

(3)每個uid請求包對應的timer觸發后,檢查Map中,查看這個uid的last_packet_time是否超過30s,如果超過則進行超時處理

輪詢掃描法:只啟動一個timer,但需要輪詢,效率較低

多timer觸發法:不需要輪詢,但每個請求包要啟動一個timer,比較耗資源

特別在同時在線量很大時,很容易CPU100%。

3. 環形隊列法

三個數據結構:

(1)30s超時,就創建一個index從0到30的環形隊列(本質上數組)

(2)環上每一個slot是一個Set<uid>任務集合

(3)同時還有一個Map<uid, index>記錄uid落在環上的哪個slot

算法:

(1)啟動一個timer,每隔1s,在上述環形隊列中移動一個,0->1->2->3...->29->30->0...

(2)有一個Current Index指針來標識剛檢測過的slot

當有某用戶uid有請求包達到時:

(1)從Map結構中,查找出這個uid存儲在哪個slot里

(2)從這個slot的Set結構中,刪除這個uid

(3)將uid重新加入到新的slot,具體是哪一個slot呢?-->Current Index指針所指向的上一個slot,因為整個slot,會被timer在30s之后掃描到

(4)更新Map,這個uid對應slot的index值

哪些元素會被超時刪除掉?

Current Index每秒移動一個slot,這個slot對應的Set<uid>中所有uid都應該被集體超時,如果最近30s有請求包來到,一定被放到Current Index的前一個slot,Current Index所在的slot對應Set中所有元素,都是最近30s沒有請求包來到的。

所以,當沒有超時時,Current Index掃描到的每一個Slot的Set中應該都沒有元素。

 

這個環形隊列法是一個通用的方法,Set和Map中可以使任何task,本文的uid是一個最簡單的舉例。

4. Netty - HashedWheelTimer

George Varghese 和 Tony Lauck 1996 年的論文:Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility提出了一種定時輪的方式來管理和維護大量的Timer調度算法.Linux 內核中的定時器采用的就是這個方案。

4.1 原理

一個Hash Wheel Timer是一個環形結構,可以想象成時鍾,分為很多格子,一個格子代表一段時間(越短Timer精度越高),並用一個List保存在該格子上到期的所有任務,同時一個指針隨着時間流逝一格一格轉動,並執行對應List中所有到期的任務,任務通過取模決定應該放入哪個格子。

環形結構可以根據超時時間的hash值(這個hash值實際上就是ticks&mask)將task分布到不同的槽位中,當tick到那個槽位時,只需要遍歷那個槽位的task即可知道哪些任務會超時(而使用線性結構,你每次tick都需要遍歷所有task)。所以,我們任務量大的時候,相應的增加wheel的ticksPerWheel值,可以減少tick時遍歷任務的個數。

以上圖為例,假設一個格子是1秒,則整個wheel能表示的時間段為8s,假如當前指針指向2,此時需要調度一個3s后執行的任務,顯然應該加入到(2 + 3 = 5)的方格中,指針再走3次就可以執行了;如果任務要在10s后執行,應該等指針走完一個round零2格再執行,因此應該放入4,同時將round(1)保存到任務中。檢查到期任務時應當只執行round為0的,格子上其他任務的round應減1。

效率:

(1)添加任務:O(1)

(2)刪除/取消任務:O(1)

(3)過期/執行任務:最差情況為O(n),也就是當HashMap里面的元素全部hash沖突,退化為一條鏈表的情況。平均O(1)

槽位越多,每個槽位上的鏈表就越短,這里需要權衡時間與空間。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM