微信公共號 [架構師之路] 上看到一篇文章 10w定時任務,如何高效觸發超時,學習了下,一些筆記。
關于 Java 定時任務,參見 Java 定時任務 & 任務調度。
業務需求
APP實時消息通道系統,對每個用戶會維護一個 APP 到服務器的 TCP 連接,用來實時收發消息,對這個 TCP 連接,有這樣一個需求:
如果連續 30s 沒有請求包(例如登錄,消息,keepalive 包),服務端就要將這個用戶的狀態置為離線
輪詢掃描法 VS 多 Timer 觸發法
方案一 輪詢掃描法:
- 用一個
Map<uid, last_packet_time>
來記錄每一個uid
最近一次請求時間last_packet_time
- 當某個用戶
uid
有請求包來到,實時更新對應的last_packet_time
- 啟動一個
Timer
,當Map
中不為空時,輪詢掃描這個 Map,看每個uid
的last_packet_time
是否超過 30s,如果超過則進行超時處理
方案二 多Timer觸發法:
- 用一個
Map<uid, last_packet_time>
來記錄每一個uid
最近一次請求時間last_packet_time
- 當某個用戶
uid
有請求包來到,實時更新對應的last_packet_time
。
并同時對這個uid
請求包啟動一個Timer
,30s 之后觸發 - 當每個
uid
請求包對應的Timer
觸發后,查看對應的last_packet_time
是否超過 30s,如果超過則進行超時處理
方案一:只啟動一個 Timer,但需要輪詢,效率較低
方案二:每個請求包要啟動一個 Timer,不需要輪詢,但比較耗資源
環形隊列法
三個重要的數據結構:
- 30s 超時,就創建一個 index 從 0 到 30 的環形隊列(本質是個數組)
- 環上每一個 slot 是一個任務集合
Set<uid>
- 同時還有一個
Map<uid, index>
,記錄uid
落在環上的哪個 slot 里
環形隊列法
同時:
- 啟動一個
Timer
,每隔 1s,在上述環形隊列中移動一格,0->1->2->3…->29->30->0…
- 有一個
Current Index
指針來標識剛檢測過的 slot
當有某用戶 uid
有請求包到達時:
- 從
Map
結構中,查找出這個uid
存儲在哪一個 slot 里
- 從這個 slot 的
Set
結構中,刪除這個uid
- 將
uid
重新加入到新的 slot 中,具體是哪一個 slot 呢?Current Index
指針所指向的上一個 slot,因為這個 slot,會被Timer
在 30s 之后掃描到 - 更新
Map
,這個uid
對應 slot 的index
值
哪些元素會被超時掉呢?
Current Index
每秒種移動一個 slot,這個 slot 對應的 Set<uid>
中所有 uid
都應該被集體超時!
如果最近 30s 有請求包來到,一定被放到 Current Index
的前一個 slot 了,Current Index
所在的 slot 對應 Set
中所有元素,都是最近 30s 沒有請求包來到的。
所以,當沒有超時時,Current Index
掃到的每一個 slot 的 Set
中應該都沒有元素。
優勢:
- 只需要1個
Timer
-
Timer
每 1s 只需要一次觸發,消耗 CPU 很低 -
批量超時,
Current Index
掃到的 slot,Set
中所有元素都應該被超時掉
總結
這個環形隊列法是一個通用的方法,Set
和 Map
中可以是任何 task
,本文的 uid
是一個最簡單的舉例。
引用:
10w定時任務,如何高效觸發超時