如果有人再問你怎麼實現分散式延時訊息,這篇文章丟給他
1.背景
上篇文章介紹了RocketMQ整體架構和原理有興趣的可以閱讀一下,在這篇文章中的延時訊息部分,我寫道開源版的RocketMQ只提供了18個層級的訊息佇列延時,這個功能在開源版中顯得特別雞肋,但是在阿里雲中的RocketMQ卻提供了支援40天之內任意秒級延時佇列,果然有些功能你只能充錢才能擁有。當然你或許想換一個開源的訊息佇列,在開源社群中訊息佇列延時訊息很多都沒有被支援比如:RabbitMQ,Kafka等,都只能透過一些特殊方法才能完成延時的功能。為什麼這麼多都沒有實現這個功能呢?是因為技術難度比較複雜嗎?接下來我們分析一下如何才能實現一個延時訊息。
2.本地延時
在實現分散式訊息佇列的延時訊息之前,我們想想我們平時是如何在自己的應用程式上實現一些延時功能的?在Java中可以透過下面的方式來完成我們延時功能:
ScheduledThreadPoolExecutor:ScheduledThreadPoolExecutor繼承了ThreadPoolExecutor,我們提交任務的時候,會將任務首先提交到DelayedWorkQueue一個優先順序佇列中,按照過期時間進行排序,這個優先順序佇列也就是我們堆結構,每次提交任務排序的複雜度是O(logN)。然後取任務的時候就會從堆頂取出我們的任務,也就是我們延遲時間最小的任務。ScheduledThreadPoolExecutor有個好處是執行延時任務可以支援多執行緒並行執行,因為他繼承的是ThreadPoolExecutor。
Timer:Timer也是利用優先順序佇列結構做的,但是其沒有繼承執行緒池,相對來說比較獨立,不支援多執行緒,只能使用單獨的一個執行緒。
3.分散式訊息佇列延時
我們實現本地延時比較簡單,直接使用Java中現成的即可,那我們分散式訊息佇列的實現有哪些難點呢?
有很多同學首先會想到我們實現分散式訊息佇列的延時任務,可不可以直接使用本地的那一套,用ScheduledThreadPoolExecutor,Timer,當然這是可以的,前提是你的訊息量很小,但是我們分散式訊息佇列往往都是企業級別的中介軟體,資料量都是非常的大,那麼我們純記憶體的方案肯定是行不通的。所以我們就有了下面這幾個方案來解決我們這個問題。
3.1 資料庫
資料庫一般來說是我們很容易想到的一個辦法,我們通常可以建立下面這樣一個表:
CREATE TABLE `delay_message` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`excute_time` bigint(16) DEFAULT NULL COMMENT '執行時間,ms級別',
`body` varchar(4096) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '訊息體',
PRIMARY KEY (`id`),
KEY `time_index` (`excute_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
這個表中我們使用excute_time代表我們真實的執行時間,並且對其建立索引,然後在我們的訊息服務中,啟動一個定時任務,定時從資料庫中掃描已經可以執行的訊息,然後開始執行,具體流程如下面所示:
使用資料庫的方法是一個比較原始的方法,在沒有延時訊息這個概念之前,要做一個訂單多少分鐘過期的這種功能,通常使用這個方法去完成。而這個方法通常也比較侷限於我們單個業務,如果想擴充套件為我們企業級的一箇中介軟體的話是不行的,因為mysql由於BTree的特性,會隨著維護二級索引的開銷越來越大,導致寫入會越來越慢,所以這個方案通常不會被考慮。
RocksDB/LevelDB
我們之前介紹RocketMQ在開源版本中只實現了18個Level的延時訊息,但是有很多公司基於RocketMQ做了自己的一套支援任意時間的延時訊息,在美團內部封裝了RocketMQ使用LevelDB做了對延時訊息的封裝,在滴滴開源的DDMQ中,使用了RocksDB對RocketMQ的延時訊息部分進行了封裝。
其原理基本和Mysql類似,如下圖所示:
Step1: DDMQ傳送訊息的時候會有一個代理層,用於將訊息做分發,因為其內部有多種訊息佇列,kafka,rocketMQ等等,如果是延時訊息會將訊息傳送到RockesDB的儲存。
Step2: 透過定時任務輪訓掃描將資料轉發投遞至RocketMQ叢集。
Step3: 消費者進行消費。
為什麼同樣是資料庫RocksDB會比Mysql更加合適呢?因為RocksDB的特性是LSM樹,其使用場景適用於大量寫入,和訊息佇列的場景更加契合,所以這個也是滴滴和美團選擇其作為延時訊息封裝的儲存介質。
3.2 時間輪+磁碟儲存
再說時間輪之前,讓我們再次回到我們的實現本地延時的時候使用的ScheduledThreadPoolExecutor還有Timer,他們都是使用的優先順序佇列完成的,優先順序佇列本質上也就是堆結構,堆結構的插入的時間複雜度是O(LogN),如果未來我們的記憶體可以做到無限,我們使用使用優先順序佇列去做延時訊息的儲存,但是隨著訊息的增多,我們的插入訊息的效率也會越來越低,那麼怎麼才能讓我們的插入訊息的效率不隨著訊息的增多而變低呢?答案就是時間輪。
什麼是時間輪呢?其實我們可以簡單的將其看做是一個多維陣列。在很多框架中都使用了時間輪來做一些定時的任務,用來替代我們的Timer,比如我之前講過的有關本地快取Caffeine一篇文章,在Caffeine中是一個二層時間輪,也就是二維陣列,其一維的資料表示較大的時間維度比如,秒,分,時,天等,其二維的資料表示該時間維度較小的時間維度,比如秒內的某個區間段。當定位到一個TimeWhile[i][j]之後,其資料結構其實是一個連結串列,記錄著我們的Node。在Caffeine利用時間輪記錄我們在某個時間過期的資料,然後去處理。
由於時間輪是一個陣列的結構,那麼其插入複雜度是O(1)。我們解決了效率之後,但是我們的記憶體依舊不是無限的,我們時間輪如何使用呢?答案當然就是磁碟,在去哪兒開源的QMQ中已經實現了時間輪+磁碟儲存,這裡為了方便描述我將其轉化為RocketMQ中的結構來進行講解,實現圖如下:
Step 1: 生產者投遞延時訊息到CommitLog,這個時候使用了偷換Topic的那招,來達到後面的效果。
Step 2: 後臺有一個Reput的任務定時拉取,延時Topic相關的Message。
Step 3: 判斷這個Message是否在當前時間輪範圍中,如果不在則來到Step4,如果在的話就直接將訊息投遞進入時間輪。
Step 4: 找到當前訊息所屬的scheduleLog,然後寫入進去,去哪兒預設劃分是一個小時為一段,這裡可以根據業務自行調整。
Step 5:時間輪會定時預載入下個時間段的scheduleLog到記憶體。
Step 6: 到點的訊息會還原topic再次投遞到CommitLog,如果投遞成功這裡會記錄dispatchLog。記錄的原因是因為時間輪是記憶體的,你不知道已經執行到哪個位置了,如果執行到最後最後1s鐘的時候掛了,這段時間輪之前的所有資料又得重新載入,這裡是用來過濾已經投遞過的訊息。
時間輪+磁碟儲存我個人覺得比上面的RocksDB要更加正統一點,不依賴其他的中介軟體就可以完成,可用性自然也就更高,當然阿里雲的RocketMQ具體怎麼實現的這個兩種方案都有可能。
3.3 redis
在社群中也有很多公司使用的Redis做的延時訊息,在Redis中有一個資料結構是Zest,也就是有序集合,他可以實現類似我們的優先順序佇列的功能,同樣的他也是堆結構,所以插入演算法複雜度依然是O(logN),但是由於Redis足夠快,所以這一塊可以忽略。(這塊沒有做對比的基準測試,只是猜測)。有同學會問,redis不是純記憶體的k,v嗎,同樣的應該也會受到記憶體限制啊,為什麼還會選擇他呢?
其實在這個場景中,Redis是很容易水平擴充套件的當一個Redis記憶體不夠,這裡可以使用兩個甚至更多,來滿足我們的需要,redis延時訊息的原理圖(原圖出自:https://www.cnblogs.com/lylife/p/7881950.html)如下:
Delayed Messages Pool: Redis Hash結構,key為訊息ID,value為具體的message,當然這裡也可以用磁碟或者資料庫代替。這裡主要儲存我們所有訊息的內容。
Delayed Queue: ZSET資料結構,value為訊息ID,score為執行時間,這裡Delayed Queue可以水平擴充套件從而增加我們可以支援的資料量。
Worker Thread Pool: 其中有多個Worker,可以部署在多個機器上形成一個叢集,叢集中的所有Worker透過ZK進行協調,分配Delayed Queue。
我們怎麼才能知道Delayed Queue中的訊息到期了呢?這裡有兩種方法:
每個Worker定時掃描,ZSET的最小執行時間,如果到了就取出,這個方法在訊息少的時候特別浪費資源,在訊息量多的時候,由於輪訓不及時導致延時的時間不準確。
因為第一個方法問題比較多,所以這裡借鑑了Timer中的一些思想,透過wait-notify可以達到一個比較好的延時效果,並且資源也不會浪費,第一次的時候還是獲取ZSET中最小的時間,然後wait(執行時間-當前時間),這樣就不需要浪費資源到達時間時會自動響應,如果當前ZSET有新的訊息進入,並且比我們等待的訊息還要小,那麼直接notify喚醒,重新獲取這個更小的訊息,然後又wait,如此迴圈。
總結
本文介紹了三種方式實現分散式延時訊息,希望能在你實現自己的延遲訊息的時候提供一點思路。總的來說可能前兩種方法來說適用面更加廣一點,畢竟在RocketMQ這些大型的訊息佇列中介軟體,還有一些其他的整合功能,比如順序訊息,事務訊息等,延時訊息可能更加傾向於是分散式訊息佇列中的一個功能,而不是作為一個獨立的元件存在。當然其中還有一些細節並沒有一一介紹,具體細節可以去參考QMQ和DDMQ的原始碼。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555607/viewspace-2672190/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 如果再有人問你分散式 ID,這篇文章丟給他分散式
- 還有人不懂分散式鎖的實現就把這篇文章丟給他分散式
- 再有人問你分散式鎖,這篇文章扔給他分散式
- 如果有人再問你 Java 的反射,把這篇文章扔給他Java反射
- 再有人問你synchronized是什麼,就把這篇文章發給他。synchronized
- 再有人問你synchronized是什麼,就把這篇文章發給他synchronized
- 再有人問你分散式事務,把這篇扔給他分散式
- 再有人問你Java記憶體模型是什麼,就把這篇文章發給他。Java記憶體模型
- 再有人問你Java記憶體模型是什麼,就把這篇文章發給他Java記憶體模型
- 面試官問你B樹和B+樹,就把這篇文章丟給他面試
- 如果有人給你撕逼Java記憶體模型,就把這些問題甩給他Java記憶體模型
- 如果有人問你資料庫的原理,叫他看這篇文章資料庫
- 延時訊息常見實現方案
- MQ不丟訊息,究竟是怎麼實現的?MQ
- 以後有面試官問你跳躍表,你就把這篇文章扔給他面試
- 以後有面試官問你「跳躍表」,你就把這篇文章扔給他面試
- 簡單延時訊息替代改造JOB實現
- 使用 Kotlin+RocketMQ 實現延時訊息KotlinMQ
- RabbitMQ實現延時訊息的兩種方法MQ
- 【漫畫】以後在有面試官問你AVL樹,你就把這篇文章扔給他。面試
- 以後再有人說程式設計師懶,請把這篇文章給他看!程式設計師
- 【漫畫】以後在有面試官問你平衡(AVL)樹,你就把這篇文章扔給他。面試
- 《RabbitMQ》 | 訊息丟失也就這麼回事MQ
- 如果有人問你爬蟲抓取技術的門道,請叫他來看這篇文章爬蟲
- 面試官問你MyBatis SQL是如何執行的?把這篇文章甩給他面試MyBatisSQL
- 為什麼你要使用這麼強大的分散式訊息中介軟體——kafka分散式Kafka
- RocketMQ定時/延時訊息MQ
- SpringBoot整合rabbitMq實現訊息延時傳送Spring BootMQ
- 如果有人讓你推薦前端技術書,請讓他看這個列表前端
- RocketMQ的訊息是怎麼丟失的MQ
- Spring註解驅動開發第16講——面試官再問你BeanPostProcessor的執行流程,就把這篇文章甩給他!Spring面試Bean
- 如果你朋友不知道什麼是雲端計算,請把這篇文章轉給TA
- 分散式訊息Kafka分散式Kafka
- 別人再問你設計模式,叫他看這篇文章設計模式
- 面試官問:Kafka 會不會丟訊息?怎麼處理的?面試Kafka
- 如果有人讓你推薦Python技術書,請讓他看這個列表Python
- 當面試官問你Vue響應式原理,你可以這麼回答他面試Vue
- 延遲訊息的五種實現方案