RocketMQ高階業務問題及解決方案

RocketMq全鏈路消息零丟失方案

  • 發送消息到mq零丟失: 事務消息
  • Broker 存儲消息零丟失:同步刷盤+主從機制
  • Consumer 消費消息零丟失:手動提交offset + 自動故障轉移

Broker消息零丟失方案:同步刷盤 + Raft協議主從同步

Broker 是負責存儲消息的,怎麼保證消息發送到Broker後,一定不會丟失呢?

刷盤失敗

首先RocketMq一般情況下,爲了保證高吞吐量,使用的是異步刷盤策略。但是這種策略會出現消息寫入os cache成功,但是異步寫入磁盤的時候失敗了。那麼這條消息就丟失了。 所以需要改成同步刷盤,使用這種策略後,只要Broker 告訴我們消息發送成功,那麼消息就一定被寫入磁盤了。

磁盤損壞

只要消息寫入到磁盤,消息就一定不會丟失嗎? 顯然不是的,如果磁盤文件損壞的話,這些消息也就丟失了。 所以必須使用 Broker 主從架構,也就是說讓一個 Master Broker 有一個 Slave Broker去同步主節點的數據,而一條消息寫入成功,必須讓Slave Broker也寫入成功,保證數據有多個副本冗餘。 這樣的話,就算Master Broker的磁盤損壞了,也還有Slave Broker的數據

Consumer消息零丟失方案:手動提交offset + 自動故障轉移

Consumer消費消息的時候,理論上將也是有可能丟失消息的。 比如當 Consumer 收到消息後,還沒進行業務處理,就直接返回成功,提交offset,接着Consumer 就崩潰了,業務還沒處理完,Broker 收到 offset 卻以爲 Consumer 消費成功了,那麼這條消息就丟失了。 所以,在RocketMq中,是先處理業務,然後最後在返回CONSUME_SUCCESS,這樣的話,即使處理業務的時候,消費者掛了,只要沒返回CONSUME_SUCCESS,Broker都認爲這個消息還沒被消費,還會再次push。 當Broker 感知到消費者掛掉的時候,它會把該機器沒處理完的消息,交給消費組裏的另一臺機器去消費,這種方式實現了故障轉移。

順序消息

通常情況下MQ的消息是無序的,因爲MQ會根據算法把消息發送到不同的MessageQueue,Consumer也會開啓多線程去進行消費,因此並不難保證消息有序。 但是,在一些場景,我們又需要消息有序。例如訂單create update delete這幾個狀態要發送消息給其他業務方,業務方希望消息嚴格按照訂單這幾個狀態的順序來發送,否則可能會出現先更新訂單,發下訂單不存在的情況。

同一個MessageQueue

如果要想保證消息有序,首先要讓同一個訂單的消息都進入到同一個MessageQueue中,MessageQueue是先進先出的,可以保證訂單的消息在該隊列中有序。 那麼如何讓同一個訂單的消息都進入到同一個MessageQueue中呢? RocketMq提供了MessageQueueSelector類,我們可以按照訂單ID對MessageQueue的數量取模,然後發送消息的時候指定這個MessageQueue。

Consumer單線程消費

雖然我們保證了同一個訂單ID下的消息都在同一個MessageQueue中,但是Consumer默認是開啓多線程消費的,如果消費訂單創建消息的時候超時了,那麼還是不能保證消息有序。 RocketMq提供了MessageListenerOrderly監聽器,該類可以保證對每一個MessageQueue都使用一個線程去消費。底層是通過ConcurrentHashMap來加鎖,使得同一時間只有一個線程可以消費。

public class MessageQueueLock {
    private ConcurrentMap<MessageQueue, Object> mqLockTable =
        new ConcurrentHashMap<MessageQueue, Object>();

    public Object fetchLockObject(final MessageQueue mq) {
        Object objLock = this.mqLockTable.get(mq);
        if (null == objLock) {
            objLock = new Object();
            Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock);
            if (prevLock != null) {
                objLock = prevLock;
            }
        }

        return objLock;
    }
}
複製代碼
//ConsumeMessageOrderlyService.java
    final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
            synchronized (objLock) {
            //...
            }
複製代碼

Consumer消費失敗怎麼辦

思考一個問題,如果consumer消費的時候失敗了,我們返回重試,這時候消息的順序就亂了,這種情況要如何處理呢? RocketMQ提供了
SUSPEND_CURRENT_QUEUE_A_MOMENT狀態,當返回這個狀態後,MQ會暫停一段時間再消息,不會把消息放入重試隊列。

延遲消息

RocketMQ提供了延遲消息的功能,非常方便。其實它的實現原理就是給延遲的消息新開一個隊列。

  • 消息生產者發送消息,如果發送的消息DelayTimeLevel大於0,則改變消息主題爲SCHEDULE_TOPIC_XXXX,消息的隊列爲DelayTimeLevel-1。
  • 消息存儲到SCHEDULE_TOPIC_XXXX上,把原有主題設置成屬性。
  • 定時任務DeliverDelayedMessageTimerTask每隔1秒從SCHEDULE_TOPIC_XXXX獲取消息。一個 task 處理一個級別的延時消息
  • 根據消息的屬性重新創建消息,並恢復原主題TopicTest、原消息隊列ID,清除DelayTimeLevel屬性存入Commitlog中,供消費者消費。

MQ消息中有百萬積壓怎麼處理

假設在訂單場景中,我們的消費者掛掉了,而訂單量是很巨大的,在短時間內就堆積了幾百萬條消息,這種情況該怎麼處理呢?

  • 根據MessageQueue的數量,擴充消費者機器 需要注意的是,機器和線程數量增大後,可能會對數據庫造成成倍的壓力!
  • 增加消費者線程數
  • 如果不能增加機器,則修改代碼,新增一個 Topic,把積壓的消息寫入新的Topic中,部署多臺Consumer去消費新的Topic

消息隊列崩潰怎麼辦

在一些金融級場景,由於涉及到金錢,因此服務一定要高可用,但是如果我們的消息隊列崩潰了,服務卻依賴消息隊列發送消息,這時要怎麼處理呢? 針對這種場景,通常要在生產者的系統中設計高可用的降級方案,比如在發送MQ的代碼裏try catch捕獲異常,如果發現有異常,進行重試。 如果發現超過3次都是失敗的,這時候可能MQ已經崩潰了,此時必須把這條消息進行持久化,可以存儲到DB、nosql(如redis的list結構)、磁盤文件中等等。 然後開啓一個後臺定時任務,去嘗試把失敗持久化的消息重新發送到MQ。

這裏必須按照順序發送,存儲時也要按照順序存儲

爲什麼要給RocketMQ增加消息限流功能保證其高可用性

限流功能其實是對MQ系統的一種保護。 如果某個程序員代碼裏寫了個bug,死循環不停的往MQ裏寫消息,並且如果有10臺機器的話,那可能沒一會MQ系統就被打掛了,影響到其他業務系統的使用。 因此,一般可以先通過壓測測一下你的MQ最多可以抗多少QPS,然後做好限流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章