簡單說說Kafka中的時間輪演算法

2021-08-30 02:28:23 字數 4714 閱讀 6490

簡單說說時間輪吧,它是乙個高效的延時佇列,或者說定時器。實際上現在網上對於時間輪演算法的解釋很多,定義也很全,這裡引用一下朱小廝部落格裡出現的定義:

參考下圖,kafka中的時間輪(timingwheel)是乙個儲存定時任務的環形佇列,底層採用陣列實現,陣列中的每個元素可以存放乙個定時任務列表(timertasklist)。timertasklist是乙個環形的雙向鍊錶,鍊錶中的每一項表示的都是定時任務項(timertaskentry),其中封裝了真正的定時任務timertask。

如果你理解了上面的定義,那麼就不必往下看了。但如果你第一次看到和我一樣懵比,並且有不少疑問,那麼這篇博文將帶你進一步了解時間輪,甚至理解時間輪演算法。

如果有興趣,可以去看看其他的定時器 你真的了解延時佇列嗎。博主認為,時間輪定時器最大的優點:

假設我們現在有乙個很大的陣列,專門用於存放延時任務。它的精度達到了毫秒級!那麼我們的延遲任務實際上需要將定時的那個時間簡單轉換為毫秒即可,然後將定時任務存入其中:

比如說當前的時間是2018/10/24 19:43:45,那麼就將任務存入task[1540381425000],value則是定時任務的內容。

private task[很長] tasks;

public listgettasklist(long timestamp)

// 假裝這裡真的能一毫秒乙個迴圈

public void run()

}

假如這個陣列長度達到了億億級,我們確實可以這麼幹。 那如果將精度縮減到秒級呢?我們也需要乙個百億級長度的陣列。

先不說記憶體夠不夠,顯然你的定時器要這麼大的記憶體顯然很浪費。

當然如果我們自己寫乙個map,並保證它不存在hash衝突問題,那也是完全可行的。(我不確定我的想法是否正確,如果錯誤,請指出)

/* 乙個精度為秒級的延時任務管理類 */

private maptaskmap;

public listgettasklist(long timestamp)

// 新增乙個任務

public void addtask(long timestamp, task task)

tasklist.add(task);

}// 假裝這裡真的能一秒乙個迴圈

public void run()

}

拋開其他疑問,我們看看手腕上的手錶(如果沒有去找個鐘錶,或者想象乙個),是不是無論當前是什麼時間,總能用我們的表盤去表示它(忽略精度)

就拿秒錶來說,它總是落在 0 - 59 秒,每走一圈,又會重新開始。

用偽**模擬一下我們這個秒錶:

private bucket[60] buckets;// 表示60秒

public void addtask(long timestamp, task task)

public bucket getbucket(long timestamp)

// 假裝這裡真的能一秒乙個迴圈

public void run()

}

這樣,我們的時間總能落在0 - 59任意乙個bucket上,就如同我們的秒鐘總是落在0 - 59刻度上一樣,這便是時間輪的環形佇列。

但是細心的小夥伴也會發現這麼乙個問題:如果只能表示60秒內的定時任務應該怎麼儲存與取出,那是不是太有侷限性了?如果想要加入一小時後的延遲任務,該怎麼辦?

其實還是可以看一看鐘錶,對於只有三個指標的表(一般的表)來說,最大能表示12個小時,超過了12小時這個範圍,時間就會產生歧義。如果我們加多幾個指標呢?比如說我們有秒針,分針,時針,上下午針,天針,月針,年針...... 那不就能表示很長很長的一段時間了?而且,它並不需要占用很大的記憶體。

比如說秒針我們可以用乙個長度為60的陣列來表示,分針也同樣可以用乙個長度為60的陣列來表示,時針可以用乙個長度為24的陣列來表示。那麼表示一天內的所有時間,只需要三個陣列即可。

動手來做吧,我們將這個資料結構稱作時間輪,tickms表示乙個刻度,比如說上面說的一秒。wheelsize表示一圈有多少個刻度,即上面說的60。interval表示一圈能表示多少時間,即 tickms * wheelsize = 60秒。

overflowwheel表示上一層的時間輪,比如說,對於秒鐘來說,overflowwheel就表示分鐘,以此類推。

public class timewheel }}

將任務新增到時間輪中十分簡單,對於每個時間輪來說,比如說秒級時間輪,和分級時間輪,都有它自己的過期槽。也就是delayms < tickms的時候。

####一、時間到期

二、時間未到期,且delayms小於interval。

對於秒級時間輪來說,就是延遲時間小於60s,那麼肯定能找到乙個秒鐘槽扔進去。

三、時間未到期,且delayms大於interval。

對於妙級時間輪來說,就是延遲時間大於等於60s,這時候就需要借助上層時間輪的力量了,很簡單的**實現,就是拿到上層時間輪,然後類似遞迴一樣,把它扔進去。

比如說乙個有乙個延時為一年後的定時任務,就會在這個遞迴中不斷建立更上層的時間輪,直到找到滿足delayms小於interval的那個時間輪。

這裡為了不把**寫的那麼複雜,我們每一層時間輪的刻度都一樣,也就是秒級時間輪表示60秒,上面則表示60分鐘,再上面則表示60小時,再上層則表示60個60小時,再上層則表示60個60個60小時 = 216000小時。

也就是如果將最底層時間輪的tickms(精度)設定為1000ms。wheelsize設定為60。那麼只需要5層時間輪,可表示的時間跨度已經長達24年(216000小時)。

/**

* 新增任務到某個時間輪

*/public boolean addtask(timedtask timedtask) else else

}return true;

}/**

* 獲取或建立乙個上層時間輪

*/private timewheel getoverflowwheel() }}

return overflowwheel;

}

當然我們的時間輪還需要乙個指標的推進機制,總不能讓時間永遠停留在當前吧?推進的時候,同時類似遞迴,去推進一下上一層的時間輪。

注意:要強調一點的是,我們這個時間輪更像是電子錶,它不存在時間的中間狀態,也就是精度這個概念一定要理解好。比如說,對於秒級時間輪來說,它的精度只能保證到1秒,小於1秒的,都會當成是已到期

對於分級時間輪來說,它的精度只能保證到1分,小於1分的,都會當成是已到期

/**

* 嘗試推進一下指標

*/public void advanceclock(long timestamp) }}

上面說到,分級時間輪,精度只有分鐘級,總不能延遲1秒的定時任務和延遲59秒的定時任務同時執行吧?

有這個疑問的同學很好!實際上很好解決,只需再入時間輪即可。比如說,對於分鐘級時間輪來說,delayms為1秒和delayms為59秒的都已經過期,我們將其取出,再扔進底層的時間輪不就可以了?

1秒的會被扔到秒級時間輪的下乙個執行槽中,而59秒的會被扔到秒級時間輪的後59個時間槽中。

細心的同學會發現,我們的新增任務方法,返回的是乙個bool

public boolean addtask(timedtask timedtask)
再倒回去好好看看,新增到最底層時間輪失敗的(我們只能直接操作最底層的時間輪,不能直接操作上層的時間輪),是不是會直接返回flase?對於再入失敗的任務,我們直接執行即可。

/**

* 將任務新增到時間輪

*/public void addorsubmittask(timedtask timedtask)

}

記得我們將任務儲存在槽中嘛?比如說秒級時間輪中,有60個槽,那麼一共有60個槽。如果時間輪共有兩層,也僅僅只有120個槽。我們只需將槽扔進乙個delayedqueue之中即可。

我們輪詢地從delayedqueue取出已經過期的槽即可。(前面的所有**,為了簡單說明,並沒有引入這個delayqueue的概念,所以不用去上面翻了,並沒有。博主覺得...已經看到這裡了,應該很明白這個delayqueue的意義了。)

其實簡單來說,實際上定時任務單單使用delayqueue來實現,也是可以的,但是一旦任務的數量多了起來,達到了百萬級,千萬級,針對這個delayqueue的增刪,將非常的慢。

** 一、面向槽的delayqueue**

而對於時間輪來說,它只需要往delayqueue裡面扔各種槽即可,比如我們的定時任務長短不一,最長的跨度到了24年,這個delayqueue也僅僅只有300個元素。

** 二、處理過期的槽**

而這個槽到期後,也就是被我們從delayqueue中poll出來後,我們只需要將槽中的所有任務迴圈一次,重新加到新的槽中(新增失敗則直接執行)即可。

/**

* 推進一下時間輪的指標,並且將delayqueue中的任務取出來再重新扔進去

*/public void advanceclock(long timeout)

} catch (exception e)

}

簡單說說 angularJs 中的 MVC

mvc 是一種物件導向軟體設計的模式,設計都餓使用目的就是為了 減少軟體日益增長的複雜度,使軟體的可維護性 可復用性得到提高 mvc的設計側重於 圖形使用者介面,比如說 office qq等這些桌面應用程式。或者是基於瀏覽器的網咯應用,這些都需要大量的視窗 選單。文字類與使用者進行互動。sss m ...

簡單說說vue中的el屬性

每個vue2.0專案中我們都會看到入口檔案 即main.js 中,在生成根例項時會配置el屬性,而我們自己建立的元件中則不能配置該屬性,下面引用了官方文件中對el屬性的說明 簡單來說el的作用就是表明我們要將當前vue元件生成的例項插入到頁面的哪個元素中,el屬性的值可以是css選擇器的字串,或者直...

簡單說說U boot的修改

sdram 32mbytes ncs1 flash 8mbytes ncs0 涉及到的檔案有四個 common.h flash.c flash.h board at91rm9200dk config.mk 以下簡單的說說。一 首先讀讀uboot自帶的readme檔案,了解了乙個大概。二 看看comm...