RabbitMQ:消息丟失 | 消息重複 | 消息積壓的原因+解決方案

前言

首先說一點,企業中最常用的實際上既不是RocketMQ,也不是Kafka,而是RabbitMQ。

RocketMQ很強大,但主要是阿里推廣自己的雲產品而開源出來的一款消息隊列,其實中小企業用RocketMQ的沒有想象中那麼多。

深層次的原因在於兔寶在中小企業普及更早,經受的考驗也更久,很容易產生「回頭客」,當初隨RabbitMQ成長的一批人才如今大部分都已成爲企業中的中堅骨幹,技術選型親睞RabbitMQ的機率就更高。

至於Kafka,主要還是用在大數據和日誌採集方面,除了一些公司有特定的需求會使用外,對消息收發準確率要求較高的公司依然是以RabbitMQ作爲企業級消息隊列的首選。

工作這麼多年我自身的感受是,RabbitMQ經久不衰,除非後續其他消息中間件有與衆不同的使用體驗,否則依然是RabbitMQ的佔有率更高。

所以準備進入軟件行業的小夥伴,我建議有必要系統的先把RabbitMQ學好,然後再學習其他消息中間件擴展視野,他們的原理大同小異,是可以觸類旁通的。

 

兩個概念

RabbitMQ避免消息丟失的方法主要是利用消息確認機制和手動簽收機制,所以有必要把這兩個概念搞清楚。

1、消息確認機制

主要是生產者使用的機制,用來確認消息是否被成功消費。

配置如下:

spring: 
    rabbitmq:
        address: 192.168.x.x:xxxx
        virtual-host: /
        username: guest
        password: guest
        connection-timeout: 5000
        publisher-confirms: true # 消息成功確認
        publisher-returns: true # 消息失敗確認
        template: 
            mandatory: true # 手動簽收機制
這樣,當你實現RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback這兩個接口的方法後,就可以針對性地進行消息確認的日誌記錄,之後做進一步的消息發送補償,以達到接近100%投遞的目的。

僞代碼如下:

@Component
@Slf4j
public class RabbitMQSender implements RabbitTemplate.ConfirmCallback, 
RabbitTemplate.ReturnCallback {

    /**
     * 發送消息
     */
    public void sendOrder(Order order) {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);

        // 發送消息
        rabbitTemplate.convertAndSend(xx, xx, order, xx);
    }


    /**
     * 成功接收後的回調
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String s) {

        // 如果成功接收了,這裏可以對日誌表的消息收發狀態做更新。
        // ....

    }


    /**
     * 失敗後的回調
     */
    @Override
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {

        // 如果失敗了,這裏可以對日誌表的消息收發狀態做更新,之後通過任務調度去補償發送。
        // ....

    }
}

 

2、消息簽收機制

RabbitMQ的消息是自動簽收的,你可以理解爲快遞簽收了,那麼這個快遞的狀態就從發送變爲已簽收,唯一的區別是快遞公司會對物流軌跡有記錄,而MQ簽收後就從隊列中刪除了。

企業級開發中,RabbitMQ我們基本都開啓手動簽收方式,這樣可以有效避免消息的丟失。
前文中已經在生產者開啓了手動簽收機制,那麼作爲消費方,也要設置手動簽收。

配置如下:

spring: 
    rabbitmq:
        address: 192.168.x.x:xxxx
        virtual-host: /
        username: guest
        password: guest
        connection-timeout: 5000
        listener: 
            simple: 
                concurrency: 5 # 併發數量
                max-concurrency: 10 # 最大併發數量
                acknowledge-mode: manual # 開啓手動簽收
                prefetch: 1 # 限制每次只消費一個(一個線程),上面配置5,也就是能一次接收5個

消費監聽時,手動簽收就一行代碼,僞代碼如下:

@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel, 
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {

    // ....

    // 手動簽收
    channel.basicAck(tag, false);

}

 

消息丟失

兩個概念搞清楚後,就可以來學習消息丟失的問題和處理方案了。

1、出現原因

消息丟失的原因無非有三種:
1)、消息發出後,中途網絡故障,服務器沒收到;
2)、消息發出後,服務器收到了,還沒持久化,服務器宕機;
3)、消息發出後,服務器收到了,消費方還未處理業務邏輯,服務卻掛掉了,而消息也自動簽收,等於啥也沒幹。
這三種情況,(1) 和 (2)是由於生產方未開啓消息確認機制導致,(3)是由於消費方未開啓手動簽收機制導致。

2、解決方案

1)、生產方發送消息時,要try...catch,在catch中捕獲異常,並將MQ發送的關鍵內容記錄到日誌表中,日誌表中要有消息發送狀態,若發送失敗,由定時任務定期掃描重發並更新狀態;
2)、生產方publisher必須要加入確認回調機制,確認成功發送並簽收的消息,如果進入失敗回調方法,就修改數據庫消息的狀態,等待定時任務重發;
3)、消費方要開啓手動簽收ACK機制,消費成功纔將消息移除,失敗或因異常情況而尚未處理,就重新入隊。
其實這就是前面闡述兩個概念時已經講過的內容,也是接近100%消息投遞的企業級方案之一,主要目的就是爲了解決消息丟失的問題。

 

消息重複

1、出現原因

消息重複大體上有兩種情況會出現:
1)、消息消費成功,事務已提交,簽收時結果服務器宕機或網絡原因導致簽收失敗,消息狀態會由unack轉變爲ready,重新發送給其他消費方;
2)、消息消費失敗,由於retry重試機制,重新入隊又將消息發送出去。

2、解決方案

網上大體上能蒐羅到的方法有三種:
1)、消費方業務接口做好冪等;
2)、消息日誌表保存MQ發送時的唯一消息ID,消費方可以根據這個唯一ID進行判斷避免消息重複;
3)、消費方的Message對象有個getRedelivered()方法返回Boolean,爲TRUE就表示重複發送過來的。
我這裏只推薦第一種,業務方法冪等這是最直接有效的方式,(2)還要和數據庫產生交互,(3)有可能導致第一次消費失敗但第二次消費成功的情況被砍掉。

 

消息積壓

1、出現原因

消息積壓出現的場景一般有兩種:
1)、消費方的服務掛掉,導致一直無法消費消息;
2)、消費方的服務節點太少,導致消費能力不足,從而出現積壓,這種情況極可能就是生產方的流量過大導致。

2、解決方案

1)、既然消費能力不足,那就擴展更多消費節點,提升消費能力;
2)、建立專門的隊列消費服務,將消息批量取出並持久化,之後再慢慢消費。
(1)就是最直接的方式,也是消息積壓最常用的解決方案,但有些企業考慮到服務器成本壓力,會選擇第(2)種方案進行迂迴,先通過一個獨立服務把要消費的消息存起來,比如存到數據庫,之後再慢慢處理這些消息即可。

 

使用心得

這裏單獨講一下本人在工作中使用RabbitMQ的一些心得,希望能有所幫助。

1)、消息丟失、消息重複、消息積壓三個問題中,實際上主要解決的還是消息丟失,因爲大部分公司遇不到消息積壓的場景,而稍微有水準的公司核心業務都會解決冪等問題,所以幾乎不存在消息重複的可能;

2)、消息丟失的最常見企業級方案之一就是定時任務補償,因爲不論是SOA還是微服務的架構,必然會有分佈式任務調度的存在,自然也就成爲MQ最直接的補償方式,如果MQ一定要實現100%投遞,這種是最普遍的方案。但我實際上不推薦中小企業使用該方案,因爲憑空增加維護成本,而且沒有一定規模的項目完全沒必要,大家都小看了RabbitMQ本身的性能,比如我們公司,支撐一個,也就是三臺8核16G服務器的集羣,上線至今3年毫無壓力;

3)、不要迷信網上和培訓機構講解的生產者消息確認機制,也就是前面兩個概念中講到的ConfirmCallback和ReturnCallback,這種機制十分降低MQ性能,我們團隊曾遇到過一次流量高峯期帶來的MQ傳輸及消費性能大幅降低的情況,後來發現是消息確認機制導致,關閉後立馬恢復正常,從此以後都不再使用這種機制,MQ運行十分順暢。同時我們會建立後臺管理實現人工補償,通過識別業務狀態判斷消費方是否處理了業務邏輯,畢竟這種情況都是少數,性能和運維成本,在這一塊我們選擇了性能;

4)、我工作這些年使用RabbitMQ沒見過自動簽收方式,一定是開啓手動簽收;

5)、手動簽收方式你在網上看到的教程幾乎都是處理完業務邏輯之後再手動簽收,但實際上這種用法是不科學的,在分佈式的架構中,MQ用來解耦和轉發是非常常見的,如果是支付業務,往往在回調通知中通過MQ轉發到其他服務,其他服務如果業務處理不成功,那麼手動簽收也不執行,這個消息又會入隊發給其他消費者,這樣就可能在流量洪峯階段因爲偶然的業務處理失敗造成堵塞,甚至標題所講的三種問題同時出現,這樣就會得不償失。

不科學的用法:在處理完業務邏輯後再手動簽收,否則不簽收,就好比客人進店了你得買東西,否則不讓走。

@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel, 
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {

    // 處理業務
    doBusiness(order);

    // 手動簽收
    channel.basicAck(tag, false);

}

科學的用法:不論業務邏輯是否處理成功,最終都要將消息手動簽收,MQ的使命不是保證客人進店了必須消費,不消費就不讓走,而是客人能進來就行,哪怕是隨便看看也算任務完成。

@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel, 
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {

    try {
        // 處理業務
        doBusiness(order);
    } catch(Exception ex) {
        // 記錄日誌,通過後臺管理或其他方式人工處理失敗的業務。
    } finally {
        // 手動簽收
        channel.basicAck(tag, false);
    }

}
可能有人會問你這樣不是和自動簽收沒區別嗎,NO,你要知道如果自動簽收,出現消息丟失你連記錄日誌的可能都沒有。

另外,爲什麼一定要這麼做,因爲MQ是中間件,本身就是輔助工具,就是一個滴滴司機,保證給你送到順便說個再見就行,沒必要還下車給你搬東西。

如果強加給MQ過多壓力,只會造成本身業務的畸形。我們使用MQ的目的就是解耦和轉發,不再做多餘的事情,保證MQ本身是流暢的、職責單一的即可。

 

總結

本篇主要講了RabbitMQ的三種常見問題及解決方案,同時分享了一些作者本人工作中使用的心得,我想網上是很難找到的,如果哪一天用到了,不妨再打開看看,也許能避免一些生產環境可能出現的問題。

我總結下來就是三點:
1)、消息100%投遞會增加運維成本,中小企業視情況使用,非必要不使用;
2)、消息確認機制影響性能,非必要不使用;
3)、消費者先保證消息能簽收,業務處理失敗可以人工補償。

工作中怕的永遠不是一個技術不會使用,而是遇到問題不知道有什麼解決思路。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章