1. 程式人生 > >10w定時任務,如何高效觸發超時

10w定時任務,如何高效觸發超時

原創: 58沈劍 架構師之路 2017-03-09

一、緣起

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

例如:58到家APP實時訊息通道系統,對每個使用者會維護一個APP到伺服器的TCP連線,用來實時收發訊息,對這個TCP連線,有這樣一個需求:“如果連續30s沒有請求包(例如登入,訊息,keepalive包),服務端就要將這個使用者的狀態置為離線”。

其中,單機TCP同時線上量約在10w級別,keepalive請求包大概30s一次,吞吐量約在3000qps。

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

“輪詢掃描法”

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

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

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

“多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,比較耗資源

特別在同時線上量很大時,很容易CPU100%,如何高效維護和觸發大量的定時/超時任務,是本文要討論的問題

二、環形佇列法

廢話不多說,三個重要的資料結構:

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中應該都沒有元素。

優勢

(1)只需要1個timer

(2)timer每1s只需要一次觸發,消耗CPU很低

(3)批量超時,Current Index掃到的slot,Set中所有元素都應該被超時掉

三、總結

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

HashedWheelTimer也是類似的原理,有興趣的同學可以百度一下這個資料結構,Netty中的一個工具類,希望大家有收穫,幫忙轉發一下哈。