一、時(shí)間輪算法簡介
為了大家能夠理解下文中的代碼,我們先來簡單了解一下netty時(shí)間輪算法的核心原理

時(shí)間輪算法名副其實(shí),時(shí)間輪就是一個(gè)環(huán)形的數(shù)據(jù)結(jié)構(gòu),類似于表盤,將時(shí)間輪分成多個(gè)bucket(比如:0-8)。假設(shè)每個(gè)時(shí)間輪輪片的分隔時(shí)間段tickDuration=1s(即:指針經(jīng)過每個(gè)格子花費(fèi)時(shí)間是 1 s),當(dāng)前的時(shí)間bucket=3,那么在18秒后需要被執(zhí)行的任務(wù)需要落到((3+18)%8=5取余運(yùn)算)的5號(hào)bucket上。假如有多個(gè)需要在該時(shí)間段內(nèi)執(zhí)行的任務(wù),就會(huì)組成一個(gè)雙向鏈表。
另外針對(duì)時(shí)間輪我們要有下面的幾個(gè)認(rèn)知:
- 時(shí)間輪指針是一個(gè)Worker線程,在時(shí)間輪整點(diǎn)的時(shí)候執(zhí)行雙向鏈表中的任務(wù)。
- 時(shí)間輪算法的并不是精準(zhǔn)的延時(shí),它的執(zhí)行精度取決于每個(gè)時(shí)間輪輪片的分隔時(shí)間段tickDuration
- Worker線程是單線程,一個(gè)bucket、一個(gè)bucket的順序處理任務(wù)。所以我們的延時(shí)任務(wù)一定要做成異步任務(wù),否則會(huì)影響時(shí)間輪后續(xù)任務(wù)的執(zhí)行時(shí)間。
二、時(shí)間輪hello-world
實(shí)現(xiàn)一個(gè)延時(shí)任務(wù)的例子,需求仍然十分的簡單:你買了一張火車票,必須在30分鐘之內(nèi)付款,否則該訂單被自動(dòng)取消。訂單30分鐘不付款自動(dòng)取消,這個(gè)任務(wù)就是一個(gè)延時(shí)任務(wù)。 我們的火車票訂單取消任務(wù),從需求上看并不需要非常精準(zhǔn)的延時(shí),所以是可以使用時(shí)間輪算法來完成這個(gè)任務(wù)的。
首先通過maven坐標(biāo)引入netty
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
然后我們創(chuàng)建一個(gè)時(shí)間輪,如果是Spring的開發(fā)環(huán)境,我們可以這么做。下文中我們new了一個(gè)包含512個(gè)bucket的時(shí)間輪,每個(gè)時(shí)間輪的輪片時(shí)間間隔是100毫秒。
@Bean("hashedWheelTimer")
public HashedWheelTimer hashedWheelTimer(){
return new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 512);
}
舉例:當(dāng)用戶買火車票下單的時(shí)候,向時(shí)間輪中添加一個(gè)30分鐘的延時(shí)任務(wù)。延時(shí)任務(wù)將在30分鐘之后被執(zhí)行,下文的lambda表達(dá)式部分實(shí)現(xiàn)了一個(gè)TimerTask(task)延時(shí)任務(wù)。這個(gè)延時(shí)任務(wù)的函數(shù)體內(nèi),請一定使用異步任務(wù),即:單獨(dú)起一個(gè)線程或者使用SpringBoot異步任務(wù)線程池。因?yàn)閃orker線程是單線程的,你的任務(wù)處理時(shí)間長于tickDuration會(huì)妨礙后續(xù)時(shí)間輪輪片上的任務(wù)的執(zhí)行。
//訂單下單操作
void order(String orderInfo) {
//下單的時(shí)候,向時(shí)間輪中添加一個(gè)30分鐘的延時(shí)任務(wù)
hashedWheelTimer.newTimeout(task -> {
//注意這里使用異步任務(wù)線程池或者開啟線程進(jìn)行訂單取消任務(wù)的處理
cancelOrder(orderInfo);
}, 30, TimeUnit.MINUTES);
}
三、異步任務(wù)線程池
我們在上文中已經(jīng)多次強(qiáng)調(diào),時(shí)間輪的任務(wù)TimerTask的執(zhí)行內(nèi)容要做成異步的。最簡單的做法就是接到一個(gè)任務(wù)之后啟動(dòng)一個(gè)線程處理該任務(wù)。在Spring環(huán)境下其實(shí)我們有更好的選擇,就是使用Spring的線程池,這個(gè)線程池是可以自定義的。比如:下文中的用法是我事先定義了一個(gè)名字為test的線程池,然后通過@Async使用即可。
@Async("test")
public void cancelOrder(String orderInfo){
//查詢訂單支付信息,如果用戶未支付,關(guān)閉訂單
}
可能有的朋友,還不知道該如何自定義一個(gè)Spring線程池,可以參考:我之前寫過一個(gè)SpringBoot的可觀測、易配置的線程池開源項(xiàng)目,源代碼地址:https://gitee.com/hanxt/zimug-monitor-threadpool 。我的這個(gè)zimug-monitor-threadpool開源項(xiàng)目,可以做到對(duì)線程池使用情況的監(jiān)控,我自己平時(shí)用的效果還不錯(cuò),向大家推薦一下!
四、時(shí)間輪優(yōu)缺點(diǎn)
時(shí)間輪算法實(shí)現(xiàn)延時(shí)任務(wù)的優(yōu)點(diǎn)就是,相對(duì)于使用JDK的DelayQueue,其算法上具有優(yōu)勢,執(zhí)行性能相對(duì)好一些。
其缺點(diǎn)就是所有的延時(shí)任務(wù)以及延時(shí)觸發(fā)的管理,都是在單個(gè)應(yīng)用服務(wù)的內(nèi)存中進(jìn)行的,一旦該應(yīng)用服務(wù)發(fā)生故障重啟服務(wù),時(shí)間輪任務(wù)數(shù)據(jù)將全部丟失。這一缺點(diǎn)和DelayQueue是一樣的。為了解決這個(gè)問題,我們可以使用redis、RocketMQ等分布式中間件來管理延時(shí)任務(wù)消息的方式來實(shí)現(xiàn)延時(shí)任務(wù),這個(gè)我會(huì)在后續(xù)的文章中為大家介紹。
歡迎關(guān)注我的公告號(hào):字母哥雜談,回復(fù)003贈(zèng)送作者專欄《docker修煉之道》的PDF版本,30余篇精品docker文章。字母哥博客:zimug.com