優(yōu)雅實(shí)現(xiàn)延時(shí)任務(wù)之Redis篇

什么是延時(shí)任務(wù)

延時(shí)任務(wù),顧名思義剃幌,就是延遲一段時(shí)間后才執(zhí)行的任務(wù)煌贴。舉個(gè)例子,假設(shè)我們有個(gè)發(fā)布資訊的功能锥忿,運(yùn)營(yíng)需要在每天早上7點(diǎn)準(zhǔn)時(shí)發(fā)布資訊,但是早上7點(diǎn)大家都還沒上班怠肋,這個(gè)時(shí)候就可以使用延時(shí)任務(wù)來實(shí)現(xiàn)資訊的延時(shí)發(fā)布了敬鬓。只要在前一天下班前指定第二天要發(fā)送資訊的時(shí)間,到了第二天指定的時(shí)間點(diǎn)資訊就能準(zhǔn)時(shí)發(fā)出去了笙各。如果大家有運(yùn)營(yíng)過公眾號(hào)钉答,就會(huì)知道公眾號(hào)后臺(tái)也有文章定時(shí)發(fā)送的功能¤厩溃總而言之数尿,延時(shí)任務(wù)的使用還是很廣泛的。關(guān)于延時(shí)任務(wù)的實(shí)現(xiàn)方式惶楼,我知道的就不下于3種右蹦,后面會(huì)逐一介紹,今天就講下如何用redis實(shí)現(xiàn)延時(shí)任務(wù)歼捐。

延時(shí)任務(wù)的特點(diǎn)

在介紹具體方案之前何陆,我們不妨先想一下要實(shí)現(xiàn)一個(gè)延時(shí)系統(tǒng),有哪些內(nèi)容是必須存儲(chǔ)下來的(這里的存儲(chǔ)不一定是指持久化豹储,也可以是放在內(nèi)存中贷盲,取決于延時(shí)任務(wù)的重要程度)。首先要存儲(chǔ)的就是任務(wù)的描述剥扣。假如你要處理的延時(shí)任務(wù)是延時(shí)發(fā)布資訊巩剖,那么你至少要存儲(chǔ)資訊的id吧铝穷。另外,如果你有多種任務(wù)類型佳魔,比如:延時(shí)推送消息曙聂、延時(shí)清洗數(shù)據(jù)等等,那么你還需要存儲(chǔ)任務(wù)的類型吃引。以上總總筹陵,都?xì)w屬于任務(wù)描述。除此之外镊尺,你還必須存儲(chǔ)任務(wù)執(zhí)行的時(shí)間點(diǎn)吧朦佩,一般來說就是時(shí)間戳。此外庐氮,我們還需要根據(jù)任務(wù)的執(zhí)行時(shí)間進(jìn)行排序语稠,因?yàn)檠訒r(shí)任務(wù)隊(duì)列里的任務(wù)可能會(huì)有很多,只有到了時(shí)間點(diǎn)的任務(wù)才應(yīng)該被執(zhí)行弄砍,所以必須支持對(duì)任務(wù)執(zhí)行時(shí)間進(jìn)行排序仙畦。

使用Redis實(shí)現(xiàn)延時(shí)任務(wù)

以上就是一個(gè)延遲任務(wù)系統(tǒng)必須具備的要素了∫羯簦回到redis慨畸,有什么數(shù)據(jù)結(jié)構(gòu)可以既存儲(chǔ)任務(wù)描述,又能存儲(chǔ)任務(wù)執(zhí)行時(shí)間衣式,還能根據(jù)任務(wù)執(zhí)行時(shí)間進(jìn)行排序呢寸士?想來想去,似乎只有Sorted Set了碴卧。我們可以把任務(wù)的描述序列化成字符串弱卡,放在Sorted Set的value中,然后把任務(wù)的執(zhí)行時(shí)間戳作為score住册,利用Sorted Set天然的排序特性婶博,執(zhí)行時(shí)刻越早的會(huì)排在越前面。這樣一來荧飞,我們只要開一個(gè)或多個(gè)定時(shí)線程凡人,每隔一段時(shí)間去查一下這個(gè)Sorted Set中score小于或等于當(dāng)前時(shí)間戳的元素(這可以通過zrangebyscore命令實(shí)現(xiàn)),然后再執(zhí)行元素對(duì)應(yīng)的任務(wù)即可叹阔。當(dāng)然划栓,執(zhí)行完任務(wù)后,還要將元素從Sorted Set中刪除条获,避免任務(wù)重復(fù)執(zhí)行忠荞。如果是多個(gè)線程去輪詢這個(gè)Sorted Set,還有考慮并發(fā)問題,假如說一個(gè)任務(wù)到期了委煤,也被多個(gè)線程拿到了堂油,這個(gè)時(shí)候必須保證只有一個(gè)線程能執(zhí)行這個(gè)任務(wù),這可以通過zrem命令來實(shí)現(xiàn)碧绞,只有刪除成功了府框,才能執(zhí)行任務(wù),這樣就能保證任務(wù)不被多個(gè)任務(wù)重復(fù)執(zhí)行了讥邻。

接下來看代碼迫靖。首先看下項(xiàng)目結(jié)構(gòu):

一共4個(gè)類:Constants類定義了Redis key相關(guān)的常量。DelayTaskConsumer是延時(shí)任務(wù)的消費(fèi)者兴使,這個(gè)類負(fù)責(zé)從Redis拉取到期的任務(wù)系宜,并封裝了任務(wù)消費(fèi)的邏輯。DelayTaskProducer則是延時(shí)任務(wù)的生產(chǎn)者发魄,主要用于將延時(shí)任務(wù)放到Redis中盹牧。RedisClient則是Redis客戶端的工具類。

最主要的類就是DelayTaskConsumer和DelayTaskProducer了励幼。

我們先來看下生產(chǎn)者DelayTaskProducer:

代碼很簡(jiǎn)單汰寓,就是將任務(wù)描述(為了方便,這里只存儲(chǔ)資訊的id)和任務(wù)執(zhí)行的時(shí)間戳放到Redis的Sorted Set中苹粟。

接下來是延時(shí)任務(wù)的消費(fèi)者DelayTaskConsumer:

public class DelayTaskConsumer {

private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

public void start(){

scheduledExecutorService.scheduleWithFixedDelay(new DelayTaskHandler(),1,1, TimeUnit.SECONDS);

}

public static class DelayTaskHandler implements Runnable{

@Override

public void run() {

Jedis client = RedisClient.getClient();

try {

Set ids = client.zrangeByScore(Constants.DELAY_TASK_QUEUE, 0, System.currentTimeMillis(),

0, 1);

if(ids==null||ids.isEmpty()){

return;

}

for(String id:ids){

Long count = client.zrem(Constants.DELAY_TASK_QUEUE, id);

if(count!=null&&count==1){

System.out.println(MessageFormat.format("發(fā)布資訊有滑。id - {0} , timeStamp - {1} , " +

"threadName - {2}",id,System.currentTimeMillis(),Thread.currentThread().getName()));

}

}

}finally {

client.close();

}

}

}

}

首先看start方法。在這個(gè)方法里面我們利用Java的ScheduledExecutorService開了一個(gè)調(diào)度線程池嵌削,這個(gè)線程池會(huì)每隔1秒鐘調(diào)度DelayTaskHandler中的run方法毛好。

DelayTaskHandler類就是具體的調(diào)度邏輯了。主要有2個(gè)步驟掷贾,一個(gè)是從Redis Sorted Set中拉取到期的延時(shí)任務(wù),另一個(gè)是執(zhí)行到期的延時(shí)任務(wù)荣茫。拉取到期的延時(shí)任務(wù)是通過zrangeByScore命令實(shí)現(xiàn)的想帅,處理多線程并發(fā)問題是通過zrem命令實(shí)現(xiàn)的。代碼不復(fù)雜啡莉,這里就不多做解釋了港准。

接下來測(cè)試一下:

我們首先生產(chǎn)了4個(gè)延時(shí)任務(wù),執(zhí)行時(shí)間分別是程序開始運(yùn)行后的5秒咧欣、10秒浅缸、15秒、20秒魄咕,然后啟動(dòng)了10個(gè)消費(fèi)者去消費(fèi)延時(shí)任務(wù)衩椒。運(yùn)行效果如下:

可以看到,任務(wù)確實(shí)能夠在相應(yīng)的時(shí)間點(diǎn)左右被執(zhí)行,不過有少許時(shí)間誤差毛萌,這個(gè)是因?yàn)槲覀兝〉狡谌蝿?wù)是通過定時(shí)任務(wù)拉取而不是實(shí)時(shí)推送的苟弛,而且拉取任務(wù)時(shí)有一部分網(wǎng)絡(luò)開銷,再者阁将,我們的任務(wù)處理邏輯是同步處理的膏秫,需要上一次的任務(wù)處理完,才能拉取下一批任務(wù)做盅,這些因素都會(huì)造成延時(shí)任務(wù)的執(zhí)行時(shí)間產(chǎn)生偏差缤削。

總結(jié)

以上就是通過Redis實(shí)現(xiàn)延時(shí)任務(wù)的思路了。這里提供的只是一個(gè)最簡(jiǎn)單的版本吹榴,實(shí)際上還有很多地方可以優(yōu)化亭敢。比如,我們可以把任務(wù)的處理邏輯再放到單獨(dú)的線程池中去執(zhí)行腊尚,這樣的話任務(wù)消費(fèi)者只需要負(fù)責(zé)任務(wù)的調(diào)度就可以了吨拗,好處就是可以減少任務(wù)執(zhí)行時(shí)間偏差。還有就是婿斥,這里為了方便劝篷,任務(wù)的描述存儲(chǔ)的只是任務(wù)的id,如果有多種不同類型的任務(wù)民宿,像前面說的發(fā)送資訊任務(wù)和推送消息任務(wù)娇妓,那么就要通過額外存儲(chǔ)任務(wù)的類型來進(jìn)行區(qū)分,或者使用不同的Sorted Set來存放延時(shí)任務(wù)了活鹰。

除此之外哈恰,上面的例子每次拉取延時(shí)任務(wù)時(shí),只拉取1個(gè)志群,如果說某一個(gè)時(shí)刻要處理的任務(wù)數(shù)非常多着绷,那么會(huì)有一部分任務(wù)延遲比較嚴(yán)重,這里可以優(yōu)化成每次拉取不止1個(gè)到期的任務(wù)锌云,比如說10個(gè)荠医,然后再逐個(gè)進(jìn)行處理,這樣的話可以極大地提升調(diào)度效率桑涎,因?yàn)槿绻鞘褂蒙厦娴姆椒ū蛳颍?0個(gè)任務(wù)需要10次調(diào)度,每次間隔1秒攻冷,總共需要10秒才能把10個(gè)任務(wù)拉取完娃胆,如果改成一次拉取10個(gè),只需要1次就能完成了等曼,效率提升還是挺大的里烦。

當(dāng)然還可以從另一個(gè)角度來優(yōu)化凿蒜。大家看上面的代碼,當(dāng)拉取到待執(zhí)行任務(wù)時(shí)招驴,就直接執(zhí)行任務(wù)篙程,任務(wù)執(zhí)行完該線程也就退出了,但是這個(gè)時(shí)候别厘,隊(duì)列里可能還有很多待執(zhí)行的任務(wù)(因?yàn)槲覀兝∪蝿?wù)時(shí)虱饿,限制了拉取的數(shù)量),所以其實(shí)在這里可以使用循環(huán)触趴,當(dāng)拉取不到待執(zhí)行任務(wù)時(shí)氮发,才結(jié)束調(diào)度,當(dāng)有任務(wù)時(shí)冗懦,執(zhí)行完還有順便查詢下有沒有堆積的任務(wù)爽冕,直到?jīng)]有堆積任務(wù)了才結(jié)束線程。

最后一個(gè)需要考慮的地方是披蕉,上面的代碼并沒有對(duì)任務(wù)執(zhí)行失敗的情況進(jìn)行處理颈畸,也就是說如果某個(gè)任務(wù)執(zhí)行失敗了,那么連重試的機(jī)會(huì)都沒有没讲。因此眯娱,在生產(chǎn)環(huán)境使用時(shí),還需要考慮任務(wù)處理失敗的情況爬凑。有一個(gè)簡(jiǎn)單的方法是在任務(wù)處理時(shí)捕獲異常徙缴,當(dāng)在處理過程中出現(xiàn)異常時(shí),就將該任務(wù)再放回Redis Sorted中嘁信,或者由當(dāng)前線程再重試處理于样。不過這樣做的話,任務(wù)的時(shí)效性就不能保證了潘靖,有可能本來定在早上7點(diǎn)執(zhí)行的任務(wù)穿剖,因?yàn)槭≈卦嚨脑颍舆t到7點(diǎn)10分才執(zhí)行完成卦溢,這個(gè)要根據(jù)業(yè)務(wù)來進(jìn)行權(quán)衡糊余,比如可以在任務(wù)描述中增加重試次數(shù)或者是否允許重試的字段,這樣在任務(wù)執(zhí)行失敗時(shí)既绕,就能根據(jù)不同的任務(wù)采取不同的補(bǔ)償措施了啄刹。

那么使用redis實(shí)現(xiàn)延時(shí)任務(wù)有什么優(yōu)缺點(diǎn)呢涮坐??jī)?yōu)點(diǎn)就是可以滿足吞吐量凄贩。缺點(diǎn)則是存在任務(wù)丟失的風(fēng)險(xiǎn)(當(dāng)redis實(shí)例掛了的時(shí)候)。因此袱讹,如果對(duì)性能要求比較高疲扎,同時(shí)又能容忍少數(shù)情況下任務(wù)的丟失昵时,那么可以使用這種方式來實(shí)現(xiàn)。

喜歡小編就輕輕關(guān)注一下吧椒丧!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末壹甥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子壶熏,更是在濱河造成了極大的恐慌句柠,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棒假,死亡現(xiàn)場(chǎng)離奇詭異溯职,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)帽哑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門谜酒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人妻枕,你說我怎么就攤上這事僻族。” “怎么了屡谐?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵述么,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我康嘉,道長(zhǎng)碉输,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任亭珍,我火速辦了婚禮敷钾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肄梨。我一直安慰自己阻荒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布众羡。 她就那樣靜靜地躺著侨赡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粱侣。 梳的紋絲不亂的頭發(fā)上羊壹,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音齐婴,去河邊找鬼油猫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛柠偶,可吹牛的內(nèi)容都是我干的情妖。 我是一名探鬼主播睬关,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼毡证!你這毒婦竟也來了电爹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤料睛,失蹤者是張志新(化名)和其女友劉穎丐箩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恤煞,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡雏蛮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阱州。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挑秉。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖苔货,靈堂內(nèi)的尸體忽然破棺而出犀概,到底是詐尸還是另有隱情,我是刑警寧澤夜惭,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布姻灶,位于F島的核電站,受9級(jí)特大地震影響诈茧,放射性物質(zhì)發(fā)生泄漏产喉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一敢会、第九天 我趴在偏房一處隱蔽的房頂上張望曾沈。 院中可真熱鬧,春花似錦鸥昏、人聲如沸塞俱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)障涯。三九已至,卻和暖如春膳汪,著一層夾襖步出監(jiān)牢的瞬間唯蝶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工遗嗽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粘我,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓媳谁,卻偏偏與公主長(zhǎng)得像涂滴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晴音,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容

  • 本文將從Redis的基本特性入手锤躁,通過講述Redis的數(shù)據(jù)結(jié)構(gòu)和主要命令對(duì)Redis的基本能力進(jìn)行直觀介紹搁料。之后概...
    kelgon閱讀 61,133評(píng)論 23 626
  • 一直打算給王濤寫一篇文章郭计,不過卻遲遲都沒有動(dòng)筆,若說起原因椒振,想來應(yīng)該是由于我總也抓不住那樣一個(gè)特別的昭伸,能夠...
    七音聽雨閱讀 867評(píng)論 1 3
  • 白茶的清香淡雅深受茶友們追捧 它體現(xiàn)的是一種態(tài)度 更是一種品格 高貴優(yōu)雅 茶友分享一款方大師制作2017年牡丹 干...
    玥燁閱讀 342評(píng)論 0 1
  • 詩(shī)歌是用來誦讀的 我等不及 就用眼睛讀了 單一個(gè)好聽的名字 ‘云雀叫了一整天’ 就吸引住我了 寥寥一句話 有聲音 ...
    阿貴的生活館閱讀 445評(píng)論 0 0
  • 像隕石墜入大地 我遇見你 輕巧的砸出扎實(shí)的烙印 不用刻意銘記 就在那里 像孩童望見的第一眼世界 自然探求的欲望 無...
    袁子姐姐閱讀 181評(píng)論 0 4