延遲訊息
延遲等級
官方預設設定了 18 哥延遲等級
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
傳送延遲訊息:按照預設順序 1-18 數字就對應上面的延遲時間
Message msg = new Message (TOPIC, TAG, "OrderID199", "ok", getBytes(StandardCharsets.UTF_8));
//設定延遲等級
msg.setDelayTimeLevel(3);
producer.send(msg);
基本原理
延遲訊息都會被儲存到 RocketMQ 的一個內部 Topic:SCHEDULE_TOPIC_XXXX 中
SCHEDULE_TOPIC_XXXX 共有 18 個 MessageQueue:
- 對應延遲訊息的 18 個等級,根據指定的 DelayTimeLevel 來決定選擇哪個 MessageQueue
- 有一個定時任務,每 100 ms 執行一次判斷 SCHEDULE_TOPIC_XXXX Topic 中的 MessageQueue 的訊息是否到達延遲時間
- 若到達延遲時間,將 SCHEDULE_TOPIC_XXXX 中的訊息投遞到訊息最初需要投遞的 Topic 之中
為什麼不支援任意時間?
RocketMQ 並不支援任意時間的延遲,可能的主要原因就是如果提供任意時間,就會涉及到訊息的排序,會有一定的效能損耗
事務訊息
RocketMQ 採用了 2PC 的思想來實現了提交事務訊息,同時增加一個補償邏輯來處理二階段超時或失敗的訊息
基本流程
第一階段:
- 傳送 Message,Half Message ,即半事務訊息
- 此型別的 Message 是不會被 Consumer 消費的
第二階段:如果半事務訊息投遞成功,則會開始執行本地事務
分為如下三種 Case:
- 本地事務執行成功:會為 Broker 傳送 commit 訊息,被 commit 過後的 Message 才能被 Consumer 消費
- 本地事務執行失敗:
- 會為 Broker 傳送 rollback 訊息,Broker 則會將剛剛投遞的半事務訊息刪除,從而保證上下游資料的一致性
- 如果 Producer 例項或者網路出現問題,Producer 沒能及時 de 將本地事務執行的結果通知 Broker,Broker 會透過掃描發現某條 Message 長時間處於半事務狀態,Broker 會主動 de 給 Producer 詢問此 Message 對應的事務狀態
基本設計
採用 2PC 兩階段設計:
將 Message 原本真實的 Topic 和 MessageQueue 進行備份
- 存入到 PROPERTY_REALTOPC、PROPERTY_REAL_QUEUE_ID 中
將訊息投遞到一個內部 Topic 中 RMQ_SYS_TRANS_HALF_TOPIC,該佇列專門儲存事務訊息
所有的 Half Message 全部都寫入到 queueId 為 0 的 MessageQueue,因為一個 Topic 下只有 1 個 MessageQueue:
- 這個 Topic 下的所有 Message 就是全域性有序的,ta 們會按照先來後到的順序被消費
如果本地事務執行成功進行 Commit,則將 RMQ_SYS_TRANS_HALF_TOPIC 佇列中的訊息投遞到真實的 Topic 中,供後續流程執行
- 並刪除這條 Half Message,但刪除也是假刪除,只是給 Message 打上一個刪除的 tag
如果本地事務執行失敗進行 rollback,則直接刪除這條 Half Message,但刪除也是假刪除
如果本地事務吃吃沒有返回結果(預設時間 6s),則會觸發事務回查機制
- 執行回查之前需要校驗檢查次數是否達到了最大值(需要手動設定,沒有預設值)
- 或者是當前 Half Message 存在是否超過了 Message 儲存的上限,即 3 天
- 如果滿足上面條件中的一種 Half Message 會被放進 TRANS_CHECK_MAX_TOPIC Topic 中
- 一旦判定為需要執行事務回查邏輯,那麼當前這條 Half Message 就算已經被消費了
- 在沒有達到最大的校驗次數之前,都還需要將其投遞到事務佇列當中,以便下次重試時再次執行 Check 邏輯
- 如果回查成功,則刪除投遞的 Half Message
訊息重試
重試時間
訊息消費失敗後,並不會立即重試,而是一個遞增的時間間隔來進行重試的,重試次數預設為 16 次
只比延遲訊息的時間間隔等級少了前兩個,延遲訊息總共有 18 個等級,而訊息重試使用了延遲訊息的第 3-18 等級
10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
基本原理
重試的 Message,RocketMQ 的做法並不是將其投遞迴原來的 Topic,而是重試佇列
每個 ConsumerGroup 都有自己的重試佇列:
- 其名稱是由特定的字首拼接上 ConsumerGroup 所組成,預設 %RETR% + 消費者組名稱
- 所有在 Consumer 啟動時,就會同時消費其 ConsumerGroup 對應的重試佇列和普通佇列
消費失敗的 Message,Consumer 會將其投回 Broker:
- 相當於這條 Message 已經被消費掉了,之後重試的只是內容相同,但實際不是同一個的 Message
- 然後會校驗重試的次數,如果達到 16 次,則會進入死信佇列,組成為 %DLQ% + 消費者組名稱
- 未達到最大重試次數,則會根據重試間隔時間等級將其投遞到延遲佇列 SCHEDULE_TOPIC_XXXX 中
- 然後等到了延遲等級對應的時間後,在投遞到 ConsumerGroup 所對應的重試佇列當中,供後續消費
訊息儲存
整體架構
RocketMQ 的混合性儲存結構(多個 Topic 的訊息實體內容都儲存於一個 CommitLog中)
針對 Producer 和 Consumer 分別採用了資料和索引部分相分離的儲存結構
Producer 傳送訊息至 Broker 端,然後 Broker 端使用同步或者非同步的方式對訊息刷盤持久化,儲存至 CommitLog 中
核心步驟:
- 首先,生產者根據 topic 傳送訊息,訊息儲存在 commitLog中,1 G一個檔案,當檔案滿了,寫入下一個檔案
- 其次,ReputMessageService 重寫訊息服務執行 2 個分發操作:
- 建立 ConsumerQueue 邏輯消費佇列:
- 引數:commitLogOffset 物理偏移量、msgSize 訊息長度、tagsCode tag 雜湊
- 建立 IndexFile 索引檔案:
- 以建立時的時間戳命名
- 建立 ConsumerQueue 邏輯消費佇列:
- 最後,消費者根據 topic、tag 拉取訊息消費,根據 key 查詢訊息
重要檔案
commitLog 訊息日誌:
- 訊息主體以及後設資料的儲存主體,儲存 Producer 端寫入的訊息主體內容
consumequeue 邏輯消費佇列:
- 儲存了 commitLog 的起始物理 offset,目的是提高消費的效能
indexFile 索引檔案:
- 提供了一種可以透過 key 或者時間區間來查詢訊息的方法
consumequeue 檔案:
consumequeue 檔案採取定長設計,每一個條目共 20 個位元組,分別為:
- 8 位元組的 commmitLog 物理偏移量
- 4 位元組 的訊息長度
- 8 位元組 tag hashcode
單個檔案由 30w 個條目組成,可以像陣列一樣隨機訪問每一個條目,每個 ConsumeQueue 檔案大小約 5.72M
- 預設一個 topic 對應 4 個 queueId,即 4 個 messageQueue
每個 messageQueue 資料夾下有多個 consumeQueue,所以:messageQueue 1 :N consumeQueue
通訊機制
通訊架構圖
基本通訊流程如下:
- Broker 啟動後需要完成一次將自己註冊至 NameServer 的操作,隨後每隔 30s 時間定時向 NameServer 上報 Topic 路由資訊
- 訊息生成者 Producer 作為客戶端傳送訊息時,需要根據訊息的 Topic 從本地快取的 TopicPublishInfoTable 獲取路由資訊
- 如果沒有則更新路由資訊會從 NameServer 上重新拉取,同時 Producer 會預設每隔 30s 向 NameServer 拉取一次路由資訊
- 訊息生產者 Producer 根據獲取的路由資訊選擇一個佇列(MessageQueue) 進行訊息傳送
- Broker 作為訊息的接收者接收訊息並落盤儲存
- 訊息消費者 Consumer 根據獲取的路由資訊,並再完成客戶端的負載均衡後,選擇其中的某一個或者某幾個訊息佇列來拉取訊息並進行消費
為了實現客戶端與伺服器之間高效的資料請求與接收:
- RocketMQ-Remoting 包自定義了通訊協議並在 Netty 的基礎之上擴充套件了通訊模組