1、緣起數組
不少時候,業務有定時任務或者定時超時的需求,當任務量很大時,可能須要維護大量的timer,或者進行低效的掃描。服務器
例如:58到家APP實時消息通道系統,對每一個用戶會維護一個APP到服務器的TCP鏈接,用來實時收發消息,對這個TCP鏈接,有這樣一個需求:「若是連續30s沒有請求包(例如登陸,消息,keepalive包),服務端就要將這個用戶的狀態置爲離線」。數據結構
其中,單機TCP同時在線量約在10w級別,keepalive請求包大概30s一次,吞吐量約在3000qps。工具
通常來講怎麼實現這類需求呢?ui
「輪詢掃描法」指針
1)用一個Map<uid, last_packet_time>來記錄每個uid最近一次請求時間last_packet_time隊列
2)當某個用戶uid有請求包來到,實時更新這個Map資源
3)啓動一個timer,當Map中不爲空時,輪詢掃描這個Map,看每一個uid的last_packet_time是否超過30s,若是超過則進行超時處理ast
「多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%,如何高效維護和觸發大量的定時/超時任務,是本文要討論的問題。
2、環形隊列法
廢話很少說,三個重要的數據結構:
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中全部元素都應該被超時掉
3、總結
這個環形隊列法是一個通用的方法,Set和Map中能夠是任何task,本文的uid是一個最簡單的舉例。
HashedWheelTimer也是相似的原理,有興趣的同窗能夠百度一下這個數據結構,Netty中的一個工具類,但願你們有收穫,幫忙轉發一下哈。