作者:凱易、明鍛
引言
Apache RocketMQ 誕生至今,歷經十餘年大規模業務穩定性打磨,服務了 100% 阿里集團內部業務以及阿里雲數以萬計的企業客戶。作爲金融級可靠的業務消息方案,RocketMQ 從創建之初就一直專注於業務集成領域的異步通信能力構建。
本篇將繼續業務消息集成的場景,從使用場景、應用案例、功能原理以及最佳實踐等角度介紹 RocketMQ 的定時消息功能。
點擊下方鏈接,查看直播講解:
https://yqh.aliyun.com/live/detail/29063
概念:什麼是定時消息
在業務消息集成場景中,定時消息是,生產者將一條消息發送到消息隊列後並不期望這條消息馬上會被消費者消費到,而是期望到了指定的時間,消費者纔可以消費到。
相似地,延遲消息其實是對於定時消息的另外一種解釋,指的是生產者期望消息延遲一定時間,消費者纔可以消費到。可以理解爲定時到當前時間加上一定的延遲時間。
對比一下定時消息和普通消息的流程。普通消息,可以粗略的分爲消息發送,消息存儲和消息消費三個過程。當一條消息發送到 Topic 之後,那麼這條消息就可以馬上處於等待消費者消費的狀態了。
而對於定時/延時消息來說,其可以理解爲在普通消息的基礎上疊加了定時投遞到消費者的特性。生產者發送了一條定時消息之後,消息並不會馬上進入用戶真正的Topic裏面,而是會被 RocketMQ 暫存到一個系統 Topic 裏面,當到了設定的時間之後,RocketMQ 纔會將這條消息投遞到真正的 Topic 裏面,讓消費者可以消費到。
場景:爲什麼需要使用定時消息
在分佈式定時調度觸發、任務超時處理等場景,需要實現精準、可靠的定時事件觸發。往往這類定時事件觸發都會存在以下訴求:
- 高性能吞吐:需要大量事件觸發,不能有性能瓶頸。
- 高可靠可重試:不能丟失事件觸發。
- 分佈式可擴展:定時調度不能是單機系統,需要能夠均衡的調度到多個服務負載。
傳統的定時調度方案,往往基於數據庫的任務表掃描機制來實現。大概的思路就是將需要定時觸發的任務放到數據庫,然後微服務應用定時觸發掃描數據庫的操作,實現任務撈取處理。
這類方案雖然可以實現定時調度,但往往存在很多不足之處:
- 重複掃描:在分佈式微服務架構下,每個微服務節點都需要去掃描數據庫,帶來大量冗餘的任務處理,需要做去重處理。
- 定時間隔不準確:基於定時掃描的機制無法實現任意時間精度的延時調度。
- 橫向擴展性差:爲規避重複掃描的問題,數據庫掃表的方案裏往往會按照服務節點拆分表,但每個數據表只能被單節點處理,這樣會產生性能瓶頸。
在這類定時調度類場景中,使用 RocketMQ 的定時消息可以簡化定時調度任務的開發邏輯,實現高性能、可擴展、高可靠的定時觸發能力。
- 精度高、開發門檻低:基於消息通知方式不存在定時階梯間隔。可以輕鬆實現任意精度事件觸發,無需業務去重。
- 高性能可擴展:傳統的數據庫掃描方式較爲複雜,需要頻繁調用接口掃描,容易產生性能瓶頸。消息隊列 RocketMQ 版的定時消息具有高併發和水平擴展的能力。
案例:使用定時消息實現金融支付超時需求
利用定時消息可以實現在一定的時間之後才進行某些操作而業務系統不用管理定時的狀態。下面介紹一個典型的案例場景:金融支付超時。現在有一個訂單系統,希望在用戶下單 30 分鐘後檢查用戶的訂單狀態,如果用戶還沒有支付,那麼就自動取消這筆訂單。
基於 RocketMQ 定時消息,我們可以在用戶下單之後發送一條定時到 30 分鐘之後的定時消息。同時,我們可以使用將訂單 ID 設置爲 MessageKey。當 30 分鐘之後,訂單系統收到消息之後,就可以通過訂單 ID 檢查訂單的狀態。如果用戶超時未支付,那麼就自動的將這筆訂單關閉。
原理:RocketMQ 定時消息如何實現
固定間隔定時消息
如前文介紹,定時消息的核心是如何在特定的時間把處於系統定時 Topic 裏面的消息轉移到用戶的 Topic 裏面去。
Apache RocketMQ 4.x 的版本的定時消息是先將定時消息放到按照 DelayLevel 放到 SCHEDULE_TOPIC_XXXX 這個系統的不同 Queue 裏面,然後爲每一個 Queue 啓動一個定時任務,定時的拉取消息並將到了時間的消息轉投到用戶的 Topic 裏面去。這樣雖然實現簡單,但也導致只能支持特定 DelayLevel 的定時消息。
當下,支持定時到任意秒級時間的定時消息的實現的 pr 提出到了社區,下面簡單的介紹一下其基本的實現原理。
時間輪算法
在介紹具體的實現原理之前,先介紹一下經典的時間輪算法,這是定時消息實現的核心算法。
如上所示,這是一個一圈定時爲 7 秒的時間輪,定時的最小精度的爲秒。同時,時間輪上面會有一個指向當前時間的指針,其會定時的移向下一個刻度。
現在我們想定時到 1 秒以後,那麼就將數據放到 “1” 這個刻度裏面,同時如果有多個數據需要定時到同一個時間,
那麼會以鏈表的方式添加到後面。當時間輪轉到 “1” 這個刻度之後,就會將其讀取並從鏈表出隊。那如果想定到超過時間輪一圈的時間怎麼處理呢?例如我們想定時到 14 秒,由於一圈的時間是 7 秒,那麼我們將其放在“6”這個刻度裏面。當第一次時間輪轉到“6” 時,發現當前時間小於期望的時間,那麼忽略這條數據。當第二次時間輪轉到“6”時,這個時候就會發現已經到了我們期望的 14 秒了。
任意秒級定時消息
在 RocketMQ 中,使用 TimerWheel 對於時間輪進行描述和存儲,同時使用一個 AppendOnly 的 TimerLog 記錄時間輪上面每一個刻度所對應的所有的消息。
TimerLog 記錄了一條定時消息的一些重要的元數據,用於後面定時的時間到了之後,將消息轉移到用戶的 Topic 裏面去。其中幾個重要的屬性如下:
對於 TimerWheel 來說,可以抽象的認爲是一個定長的數組,數組中的每一格代表時間輪上面的一個“刻度”。TimerWheel 的一個“刻度”擁有以下屬性。
TimerWheel 和 TimerLog 直接的關係如下圖所示:
TimerWheel 中的每一格代表着一個時間刻度,同時會有一個 firstPos 指向這個刻度下所有定時消息的首條 TimerLog 記錄的地址,一個 lastPos 指向這個刻度下所有定時消息最後一條 TimerLog 的記錄的地址。並且,對於所處於同一個刻度的的消息,其 TimerLog 會通過 prevPos 串聯成一個鏈表。
當需要新增一條記錄的時候,例如現在我們要新增一個 “1-4”。那麼就將新記錄的 prevPos 指向當前的 lastPos,即 “1-3”,然後修改 lastPos 指向 “1-4”。這樣就將同一個刻度上面的 TimerLog 記錄全都串起來了。
有了 TimerWheel 和 TimerLog 之後,我們再來看一下一條定時消息從發送到 RocketMQ 之後是怎麼最終投遞給用戶的。
首先,當發現用戶發送的是一個定時消息過後,RocketMQ 實際上會將這條消息發送到一個專門用於處理定時消息的系統 Topic 裏面去
然後在 TimerMessageStore 中會有五個 Service 進行分工合作,但整體可以分爲兩個階段:入時間輪和出時間輪
對於入時間輪:
- TimerEnqueueGetService 負責從系統定時 Topic 裏面拉取消息放入 enqueuePutQueue 等待 TimerEnqueuePutService 的處理
- TimerEnqueuePutService 負責構建 TimerLog 記錄,並將其放入時間輪的對應的刻度中
對於出時間輪:
- TimerDequeueGetService 負責轉動時間輪,並取出當前時間刻度的所有 TimerLog 記錄放入 dequeueGetQueue
- TimerDequeueGetMessageService 負責根據 TimerLog 記錄,從 CommitLog 中讀取消息
- TimerDequeuePutMessageService 負責判斷隊列中的消息是否已經到期,如果已經到期了,那麼將其投入用戶的 Topic 中,等待消費消費;如果還沒有到期,那麼重新投入系統定時 Topic,等待重新進入時間輪。
實戰:使用定時消息
瞭解了 RocketMQ 秒級定時消息的原理後,我們看下如何使用定時消息。首先,我們需要創建一個 “定時/延時消息” 類型的 Topic,可以使用控制檯或者 CLi 命令創建。
從前面可以看出,對於定時消息來說,是在發送消息的時候 “做文章”。所以,對於生產者,相對於發送普通消息,我們可以在發送的時候設置期望的投遞時間。
當定時的時間到了之後,這條消息其實就是一條投遞到用戶 Topic 的普通消息而已。所以對於消費者來說,和普通消息的消費沒有區別。
注意:定時消息的實現邏輯需要先經過定時存儲等待觸發,定時時間到達後纔會被投遞給消費者。因此,如果將大量定時消息的定時時間設置爲同一時刻,則到達該時刻後會有大量消息同時需要被處理,會造成系統壓力過大。所以一般建議儘量不要設置大量相同觸發時刻的消息。
點擊此處,進入官網瞭解更多詳情~