出處: 那些驚豔的演算法們(三)—— 時間輪
自然界中定時任務無處不在,太陽每天東昇西落,候鳥的遷徙,樹木的年輪,人們每天按時上班,每個月按時發工資、交房租,四季輪換,潮漲潮落,等等,從某種意義上說,都可以認為是定時任務。
大概很少有人想過,這些「定時」是怎樣做到的。當然,計算機領域的同學們可能對此比較熟悉,畢竟工作中的定時任務也是無處不在的:每天凌晨更新一波資料庫,每天9點發一波郵件,每隔10秒鐘搶一次火車票。。。
至於怎麼實現的?很簡單啊,作業系統的crontab,spring框架的quartz,實在不行j**a自帶的scheduledthreadpool都可以很方便的做到定時任務的管理排程。
當你熟練的敲下「* * 9 * * ?」等著神奇的事情發生時,你是否想過背後的「玄機」?
大概去年的時候,業務需要實現乙個時間排程的工具,定時生成報表,同組的哥們兒想了乙個取巧的辦法:
1. 啟動時從db讀取cron表示式解析,算出該任務下次執行的時間。
2. 下次執行的時間 - 當前時間 = 時間差。
3. 向schedulethreadpool執行緒池中提交乙個延遲上面算出來的時間差的執行的任務。
4. 任務執行時,算一下這個任務下次執行的時間,算時間差,提交到執行緒池。
5. 當任務需要取消時,直接呼叫執行緒池返回的future物件的cancel()方法就行了。
**中的思路很簡單但也十分巧妙,對演算法不斷的改進對比,各種作業系統,框架中的基於時間的排程演算法都是基於時間輪的思想實現的。下面我們來看看,這個神奇的時間輪到底是怎樣實現定時任務的排程的。
定時任務一般有兩種:
1. 約定一段時間後執行。
2. 約定某個時間點執行。
聰明的你會很快發現,這兩者之間可以相互轉換,比如給你個任務,要求12點執行,你看了一眼時間,發現現在是9點鐘,那麼你可以認為這個任務三個小時候執行。
同樣的,給你個任務讓你3個小時後執行,你看了一眼現在是9點鐘,那麼你當然可以認為這個任務12點鐘執行。
我們先來考慮乙個簡單的情況,你接到三個任務a、b、c(都轉換成絕對時間),分別需要再3點鐘,4點鐘和9點鐘執行,正當百思不得其解時,不經意間你瞅了一眼牆上的鐘錶,瞬間來了靈感,如醍醐灌頂,茅塞頓開:
如上圖中所示,**我只需要把任務放到它需要被執行的時刻,然後等著時針轉到這個時刻時,取出該時刻放置的任務,執行就可以了**。 這就是時間輪演算法最核心的思想了。 什麼?時針怎麼轉? while-true-sleep 下面讓我們一點一點增加複雜度。
多數定時任務是需要重複執行,比如每天上午9點執行生成報表的任務。對於重複執行的任務,其實我們需要關心的只是下次執行時間,並不關心這個任務需要迴圈多少次,還是那每天上午9點的這個任務來說。
1. 比如現在是下午4點鐘,我把這個任務加入到時間輪,並設定當時針轉到明天上午九點(該任務下次執行的時間)時執行。
2. 時間來到了第二天上午九點,時間輪也轉到了9點鐘的位置,發現該位置有乙個生成報表的任務,拿出來執行。
3. 同時時間輪發現這是乙個迴圈執行的任務,於是把該任務重新放回到9點鐘的位置。
4. 重複步驟2和步驟3。
如果哪一天這個任務不需要再執行了,那麼直接通知時間輪,找到這個任務的位置刪除掉就可以了。
由上面的過程我們可以看到,時間輪至少需要提供4個功能:
1. 加入任務
2. 執行任務
3. 刪除任務
4. 沿著時間刻度前進
上面說的是同乙個時刻只有乙個任務需要執行的情況,更通用的情況顯然是同一時刻可能需要執行多個任務,比如每天上午九點除了生成報表之外,還需要執行傳送郵件的任務,需要執行建立檔案的任務,還需執行資料分析的任務等等,於是你剛才可能就比較好奇的時間輪的資料結構到現在可能更加好奇了,那我們先來說說時間輪的資料結構吧。
首先,時鐘可以用陣列或者迴圈鍊錶表示,這個每個時鐘的刻度就是乙個槽,槽用來存放該刻度需要執行的任務,如果有多個任務需要執行呢?每個槽裡面放乙個鍊錶就可以了,就像下面圖中這樣:
同一時刻存在多個任務時,只要把該刻度對應的鍊錶全部遍歷一遍,執行(扔到執行緒池中非同步執行)其中的任務即可。
如果任務不只限定在一天之內呢?比如我有個任務,需要每週一上午九點執行,我還有另乙個任務,需要每週三的上午九點執行。一種很容易想到的解決辦法是:
一天24個小時,一周168個小時,為了解決上面的問題,我可以把時間輪的刻度(槽)從12個增加到168個,比如現在是星期二上午10點鐘,那麼下周一上午九點就是時間輪的第9個刻度,這週三上午九點就是時間輪的第57個刻度,示意圖如下:
仔細思考一下,會發現這中方式存在幾個缺陷:
1. 時間刻度太多會導致時間輪走到的多數刻度沒有任務執行,比如乙個月就2個任務,我得移動720次,其中718次是無用功。
2. 時間刻度太多會導致儲存空間變大,利用率變低,比如乙個月就2個任務,我得需要大小是720的陣列,如果我的執行時間的粒度精確到秒,那就更恐怖了。
於是乎,聰明的你腦袋一轉,想到另乙個辦法:
這次我不增加時間輪的刻度了,刻度還是24個,現在有三個任務需要執行,
1. 任務一每週二上午九點。
2. 任務二每週四上午九點。
3. 任務三每個月12號上午九點。
比如現在是9月11號星期二上午10點,時間輪轉一圈是24小時,到任務一下次執行(下周二上午九點),需要時間輪轉過6圈後,到第7圈的第9個刻度開始執行。
任務二下次執行第3圈的第9個刻度,任務三是第2圈的第9個刻度。
示意圖如下:
分層時間輪是這樣一種思想:
1. 針對時間複雜度的問題:不做遍歷計算round,凡是任務列表中的都應該是應該被執行的,直接全部取出來執行。
2. 針對空間複雜度的問題:分層,每個時間粒度對應乙個時間輪,多個時間輪之間進行級聯協作。
第一點很好理解,第二點有必要舉個例子來說明:
比如我有三個任務:
1. 任務一每週二上午九點。
2. 任務二每週四上午九點。
3. 任務三每個月12號上午九點。
三個任務涉及到四個時間單位:小時、天、星期、月份。
拿任務三來說,任務三得到執行的前提是,時間刻度先得來到12號這一天,然後才需要關注其更細一級的時間單位:上午9點。
基於這個思想,我們可以設定三個時間輪:月輪、周輪、天輪。
月輪的時間刻度是天。
周輪的時間刻度是天。
天輪的時間刻度是小時。
初始新增任務時,任務一新增到天輪上,任務二新增到周輪上,任務三新增到月輪上。
三個時間輪以各自的時間刻度不停流轉。
當周輪移動到刻度2(星期二)時,取出這個刻度下的任務,丟到天輪上,天輪接管該任務,到9點執行。
同理,當月輪移動到刻度12(12號)時,取出這個刻度下的任務,丟到天輪上,天輪接管該任務,到9點執行。
這樣就可以做到既不浪費空間,有不浪費時間。
整體的示意圖如下所示:
時間輪的應用
時間輪的思想應用範圍非常廣泛,各種作業系統的定時任務排程,crontab,還有基於j**a的通訊框架netty中也有時間輪的實現,幾乎所有的時間任務排程系統採用的都是時間輪的思想。
至於採用round型的時間輪還是採用分層時間輪,看實際需要吧,時間複雜度和實現複雜度的取捨。
linux定時器時間輪演算法詳解
linux高併發程式設計 紅黑樹實現定時器 時間輪實現定時器 linux多執行緒環境下海量定時任務的定時器設計 linux定時器分為低精度定時器和高精度定時器兩種型別,核心對其均有實現。本文討論的是我們在應用程式開發中比較常見的低精度定時器。作為常用的基礎元件,定時器常用的幾種實現方法包括 基於排序...
簡單說說Kafka中的時間輪演算法
簡單說說時間輪吧,它是乙個高效的延時佇列,或者說定時器。實際上現在網上對於時間輪演算法的解釋很多,定義也很全,這裡引用一下朱小廝部落格裡出現的定義 參考下圖,kafka中的時間輪 timingwheel 是乙個儲存定時任務的環形佇列,底層採用陣列實現,陣列中的每個元素可以存放乙個定時任務列表 tim...
xxl job 執行器時間輪
時間輪出自netty中的hashedwheeltimer,是乙個環形結構,可以用時鐘來模擬,鐘面上有很多bucket,每乙個bucket上可以存放多個任務,使用乙個list儲存該時刻到期的所有任務,同時乙個指標隨著時間流逝一格一格轉動,並執行對應bucket上所有到期的任務。任務通過取模決定應該放入...