文章目錄
爲什麼要用消息隊列
- 解耦:A系統調用B系統、C系統,傳統的調用是直接調用,但是當B系統說我不需要你提供數據了,這時候A需要改代碼,C系統說我不需要某個字段了,這時候A也要改代碼,如果又多了一個D系統,A又要寫代碼。爲了實現解耦,引入消息隊列,A將產生的數據丟到消息隊列中,哪個系統需要 哪個系統就去取;
- 異步:A系統調用B系統,B系統由於某個需要調用第三方接口超時,導致A系統響應速度慢,而B系統的好壞又不會影響業務邏輯,所以可以改爲A異步調用B,A將消息丟到消息隊列中,B系統訂閱消息,實現A的快速響應;
- 削峯:當大量流量請求到系統A時,由於數據庫的處理能力有限,造成數據庫連接異常。使用消息隊列,大量請求先丟到消息隊列中,系統A根據自身的處理能力批量拉取數據進行處理。生產中,高峯期短暫的消息積壓是允許的。
使用消息隊列有什麼缺點
系統複雜性增加,可用性降低。
- 系統複雜性增加:加了消息隊列,需要保證消息隊列的高可用,需要保證消息不會重複消費,需要保證消息的可靠性。
- 系統的可用性降低:如果消息隊列掛了,那麼系統也會受到影響。
RocketMQ
RocketMQ架構圖
消息生產者(Producer)
負責生產消息,一般由業務系統負責生產消息。一個消息生產者會把業務應用系統裏產生的消息發送到Broker服務器。RocketMQ提供多種發送方式,同步發送、異步發送、順序發送、單向發送。同步和異步方式均需要Broker返回確認信息,單向發送不需要。
消息消費者(Consumer)
負責消費消息,一般是後臺系統負責異步消費。一個消息消費者會從Broker服務器拉取消息,並將其提供給應用程序。從用戶應用的角度而言提供了兩種消費形式:pull拉取式消費、push推動式消費。
名稱服務器(NameServer)
NameServer 中維護着 Producer 集羣、Broker 集羣、 Consumer 集羣的服務狀態。通過定時發送心跳數據包進行維護更新各個服務的狀態。NameServer充當路由消息的提供者。生產者或消費者能夠通過名字服務查找各主題相應的Broker IP列表。多個NameServer實例組成集羣,但相互獨立,沒有信息交換。
消息代理(Broker)
負責存儲/轉發消息(轉發分爲推和拉兩種,拉是指Consumer主動從Message Broker獲取消息,推是指Message Broker主動將Consumer感興趣的消息推送給Consumer)
RocketMQ是怎麼保證系統高可用的?
- NameServer高可用
NameServer 可以部署多個,多個NameServer互相獨立,不會交換消息。Producer、Broker、Consumer 啓動的時候都需要指定多個 NameServer,各個服務的信息會同時註冊到多個 NameServer 上,從而能到達高可用。
- Broker高可用
多Master部署,防止單點故障;
主從結構,消息冗餘,防止消息丟失;
Broker的一個主從組:Master節點1+Slave節點N
Master節點提供讀寫服務,Slave節點只提供讀服務
每個主從組,Master節點 不斷髮送新的 CommitLog 給 Slave節點。 Slave節點 不斷上報本地的 CommitLog 已經同步到的位置給 Master節點。Master節點與Slave節點之間通信方式有同步和異步。
RocketMQ如何保證消息的可靠性
爲了降低消息丟失的概率,MQ需要進行超時和重傳
(1) MQ-client-sender 發送消息給MQ-server
(2) MQ-server接收到消息後,發送 ACK消息給發送方
(3) MQ-client-sender 接收到 ACK消息後,則 消息已經投遞成功
如果上述 2 消息丟失或者超時,MQ-client-sender 內的 timer 會重發消息,直到收到 ACK消息,如果重試N次後還未收到,則回調發送失敗。需要注意的是,這個過程中 MQ-server 可能會收到同一條消息的多次重發。
對每條消息,MQ系統內部必須生成一個inner-msg-id,作爲去重和冪等的依據,這個內部消息ID的特性是:
- 全局唯一
- MQ生成,具備業務無關性,對消息發送方和消息接收方屏蔽
(4) MQ-server 將消息發送給 MQ-client-receiver
(5) MQ-client-receiver 得到消息處理業務邏輯
(6) MQ-client-receiver 回覆 ACK消息給 MQ-server
(7) MQ-server收到 ACK消息,將已消費的消息刪除
如果上述 6 消息丟失或者超時,MQ-server 內的 timer 會重發消息,直到 MQ-server 收到ACK消息 並且 將已消費的消息刪除,這個過程也可能會重發多次,MQ-client-receiver 也可能會收到同一條消息的多次重發。
需要強調的是,MQ-client-receiver 回ACK給 MQ-server,是消息消費業務方的主動調用行爲,不能由 MQ-client-sender 自動發起,因爲MQ系統不知道消費方什麼時候真正消費成功。
爲了保證業務冪等性,業務消息體中,必須有一個biz-id,作爲去重和冪等的依據,這個業務ID的特性是:
- 對於同一個業務場景,全局唯一
- 由業務消息發送方生成,業務相關,對MQ透明
- 由業務消息消費方負責判重,以保證冪等
最常見的業務ID有:支付ID,訂單ID,帖子ID等。
RocketMQ如何解決消息順序
消息有序:指的是可以按照消息的發送順序來消費。例如:一筆訂單產生了 3 條消息,分別是訂單創建、訂單付款、訂單完成。消費時,要按照順序依次消費纔有意義。
如何實現順序消息:保證生產者——MQServer——消費者是一對一對一關係。
雖然這樣可以實現嚴格的順序消息,但是卻存在很大的併發性能問題,並且需要處理更多的異常。
所以RocketMQ認爲應該從業務層上來保證消息順序而不僅僅是依賴於消息系統。
不關注亂序的應用實際大量存在
隊列無序並不意味着消息無序
RocketMQ如何解決消息重複
造成消息重複的根本原因是:網絡不可達。只要通過網絡交換數據,就無法避免這個問題。
解決方法:
- 消費端處理消息的業務邏輯保持冪等性
即只要保持冪等性,不管來多少條重複消息,最後處理結果都是一樣的(消費端實現) - 保證每條消息都有唯一編號且保證消息處理成功與去重表的日誌同時出現。
可以由消息系統實現,也可以由業務端實現。由消息系統來實現,無疑會對消息系統的吞吐量和高可用有所影響,所以最好由業務端自己處理消息重複問題,這也是RocketMQ不解決消息重複的問題的原因。
RocketMQ不保證消息不重複,如果你的業務需要保證嚴格的不重複消息,需要你自己在業務端去重。
RocketMQ實現事務消息
以購物場景爲例,張三購買物品,賬戶扣款 100 元的同時,需要保證在下游的會員服務中給該賬戶增加 100 積分。而扣款的業務和增加積分的業務是在兩個不同的應用,正常處理邏輯一般是先扣除100元,然後網絡通知積分服務增加100積分。如下圖:
以上過程會存在3個問題:
- 賬號服務在扣款的時候宕機了,這時候可能扣款成功,也可能扣款失敗;
- 由於網絡穩定性無法保證,通知扣積分服務可能失敗,但是扣款成功了;
- 扣款成功,並且通知成功,但是增加積分的時候失敗了。
實際上,rocketmq的事務消息解決的是問題1和問題2這種場景,也就是解決本地事務執行與消息發送的原子性問題。即解決Producer執行業務邏輯成功之後投遞消息可能失敗的場景。
而對於問題3這種場景,rocketmq提供了消費失敗重試的機制。
事務消息的實現思路和過程
RocketMQ 事務消息的設計流程同樣借鑑了兩階段提交理論,通過在執行本地事務前後發送兩條消息來保證本地事務與發送消息的原子性,過程如下圖:
事務消息詳細過程說明
- 事務發起方首先發送 prepare 消息到 MQ。
- 在發送 prepare 消息成功後執行本地事務。
- 根據本地事務執行結果返回 commit 或者是 rollback。
- Producer發送確認消息到broker(也就是將步驟3執行的結果發送給broker),這裏可能broker未收到確認消息,下面分兩種情況分析:
- 如果broker收到了確認消息:如果收到的結果是commit,則broker視爲整個事務過程執行成功,將消息下發給Conusmer端消費;如果收到的結果是rollback,則broker視爲本地事務執行失敗,broker刪除Half消息,不下發給consumer。
- 如果broker未收到了確認消息:broker定時回查本地事務的執行結果;(由用戶實現)如果本地事務已經執行則返回commit;如果未執行,則返回rollback;
- Consumer 端的消費成功機制有 MQ 保證。
Producer發送消息
Procucer通過輪詢某topic下的所有隊列的方式來實現發送方的負載均衡,如下圖所示:
在整個應用生命週期中,生產者需要調用一次start方法來初始化,初始化主要完成的任務有:
- 如果沒有指定NameServer地址,則會自動尋址
- 啓動定時任務:更新NameServer地址,從NameServer更新Topic路由信息、清理已經掛掉的broker、向所有broker發送心跳……
- 啓動負載均衡的服務
初始化完成後,生產者則開始發送消息。如果生產者發送消息失敗,則會自動重試。
MQ存儲消息
RocketMQ的消息存儲是由consume queue和commit log配合完成的。
Comsume queue
consume queue是消息的邏輯隊列,用來指定消息在物理文件commit log上的物理位置
每個topic下的每個隊列都有一個對應的consume queue文件。
Consume Queue中存儲單元是一個20字節定長的二進制數據,順序寫順序讀,如下圖所示:
Commit log
commit log:消息存放的物理文件,每臺broker上的commit log被本機所有的queue共享,不做任何區分。
CommitLog的消息存儲單元長度不固定,文件順序寫,隨機讀。消息的存儲結構如下表所示,按照編號順序以及編號對應的內容依次存儲。
RocketMQ消息訂閱
RocketMQ消息訂閱有兩種模式,一種是Push模式,即MQServer主動向消費端推送;另外一種是Pull模式,即消費端在需要時,主動到MQServer拉取。但在具體實現時,Push和Pull模式都是採用消費端主動拉取的方式。
消費端的Push模式是通過長輪詢的模式來實現的,就如同下圖:
Consumer端每隔一段時間主動向broker發送拉消息請求,broker在收到pull請求後,如果有消息則立即返回數據,Comsumer端收到返回的消息後,再回調消費者設置的Listener方法。如果broker在收到pull請求時,消息隊列裏沒有數據,broker端會阻塞請求直到有數據傳遞或超時才返回。
當然,Consumer端是通過一個線程將阻塞隊列LinkedBlockingQueue中的PullRequest發送到broker中去拉取消息,以防止Consumer一直被阻塞。而Broker端,在接收到Consumer的PullRequest請求時,如果發現沒有消息,則將PullRequest扔到ConcurrentHashMap中緩存起來。Broker在啓動時,會啓動一個線程不停地從ConcurrentHashMap中取出PullRequest進行檢查,直到有數據返回。
消息隊列對比
消息重試:Kafka消費失敗不支持重試,RocketMQ消費失敗支持定時重試,每次重試時間順延。
消息順序:Kafka和RocketMQ都支持消息順序,但一臺broker宕機後,Kafka會產生消息亂序,而RocketMQ不會。
分佈式事務消息:Kafka不支持分佈式事務消息,而RocketMQ支持分佈式事務消息。
消息堆積:理論上Kafka的消息堆積能力比RocketMQ強,但RocketMQ單機也可以支持億級的消息堆積能力。
Kafka主要特點是基於pull的模式來處理消息消費,追求高吞吐量,支持複製,不支持事務,對消息的重複、丟失、錯誤沒有嚴格要求,適合產生大量數據的互聯網服務的數據收集業務。
RocketMQ是阿里開源的消息中間件,純java開發,具有高吞吐量、高可用性、適合大規模分佈式系統應用的特點。起源於Kafka,但其對消息的可靠傳輸和事務性做了優化,目前被廣泛應用於交易、充值、消息推送、日誌流式計算等場景。
RabbitMQ使用Erlang語言開發,基於AMQP協議來實現。主要特點是面向消息、隊列、路由、可靠性、安全。更多地被應用在企業系統內,對數據一致性、穩定性和可靠性要求很高,對性能和吞吐量要求不高的場景。
RocketMQ和ActiveMQ的區別
RocketMQ模型簡單、接口易用,在阿里大規模使用,社區活躍,單機吞吐量10萬級,可用性非常高,消息理論上不會丟失;
-
ActiveMQ嚴格遵循JMS規範,可持久化到內存、文件、數據庫,可用性高主要是主從,多語言支持,消失丟失率低;
-
RocketMQ持久化到磁盤文件,可用性非常高,支持分佈式,只支持Java,消息理論上不會丟失;