一、前言
由於工作的需要,得實現一個用於控制事件超時拋棄的時間輪,由於這是一個相對獨立的接口,就總結分享一下。
首先看下需求,此時間輪需要具備下面幾個功能:
1)能添加事件,同時附上其超時時間;
2)如果事件正常執行結束,可以顯示將其從時間輪上剔除掉,而不需要等時間輪自動移除;
3)如果事件到了設定的超時時間還沒執行完,則時間輪需將其剔除掉,並發送一個超時的消息給系統。
基於這樣的需求,下面就進行相應的設計和實現。
二、時間輪的設計
基於前面的需求,可以抽象出兩個實體來:時鍾和槽,其中時鍾去負責指針的走動、獲取等,而槽用來存儲落在各個時間點的事件。同時還需要有三個行為:添加,正常執行的刪除,超時的刪除和發送消息。有了這樣的抽象,首先設計了兩個類Clocker和Slot,分別代表時鍾和槽;對於幾種行為,則需要放到一個主控制類中,設計為TimeWheel,將接口暴露給外部調用。這樣系統的靜態結構就出來了。
注意到,當事件超時后,需要發送消息出去,以供事件執行相關的處理,這里采用一種面向接口的編程方式。我們先定義一個接口Expiration,里面只有一個方法expired方法。具體的事件需要實現這個接口,這樣在我們的時間輪中,只要超時,直接以傳入的事件來調用expired方法即能起到發送消息的目的!
對着幾部門進行組合,就有了主體框架類圖。
對於Clocker類,由於它要控制着時間的不停走動,得是一個單獨的線程去完成。其中時間的走動,最好不用Thread.sleep去模擬;可以用一個空的阻塞隊列,然后每次調用poll方法,設置間隔時間為超時時間,這樣的效果會更好。對於一個新事件,帶着超時時間來,需要找到其應在的指針位置,通常的做法就是按超時時間除以預定義好的間隔時間,獲取一個偏移量,加上當前位置即可。
對於主類TimeWheel,由於在事件正常結束的時候,可以讓事件主動從時間輪中將自己刪除掉,因此其add方法就需要返回一個事件所在slot的位置;這樣在remove的時候,連帶位置信息,就可以方便找到坐在的slot,從中刪除該事件。
當超時時間到時,需要將對應的slot中所有元素清除掉,這個工作可以在Slot類中完成,即將對應槽中所有元素取出來放到臨時變量中,將當前槽清空,這樣就能保證槽中不會有事件積壓。對於取出的元素,調用對應的expired方法即可。
基於這樣的設計,已經能夠滿足系統的需要了,並且運行的較為良好。
三、優化
對於上面的設計,細想會有一個可改造的點,就是對於超時元素執行expired方法是在TimeWheel線程里執行的,這樣會有問題:
1)不確定expired方法執行所需要花費的時間,這樣就有可能影響主線程;
2)如果expired方法出現異常,主線程可能會受到影響。
基於這樣的考慮,我們可以把這些事件放到一個阻塞隊列里,然后另起一個線程,專門去隊列中取元素,執行expired方法。這樣就很好的將expired操作和主線程隔離了開來。對於這樣的設計,也就是典型的生產者-消費者模型,主線程TimeWheel負責生產數據,設計一個Release線程負責消費數據,倉庫用阻塞隊列承擔,這樣就較好的解決了這個問題,起到了優化的作用。