RabbitMQ的訊息可靠性(五)

童話述說我的結局發表於2021-10-15

一、可靠性問題分析

訊息的可靠性投遞是使用訊息中介軟體不可避免的問題,不管是使用哪種MQ都存在這種問題,接下來要說的就是在RabbitMQ中如何解決可靠性問題;在前面

 

 在前面說過訊息的傳遞過程中有三個物件參與分別是:生產者、RabbitMQ(broker)、消費者;接下來就是要圍繞這三個物件來分析訊息在傳遞過程中會在哪些環節出來可靠性問題;

RabbitMQ訊息的可靠性投遞主要兩種實現:
1、通過實現消費的重試機制,通過@Retryable來實現重試,可以設定重試次數和重試頻率;
2、生產端實現訊息可靠性投遞。
兩種方法消費端都可能收到重複訊息,要求消費端必須實現冪等性消費。

 

 

 1.1、生產者丟失訊息

生產者傳送訊息到broker時,要保證訊息的可靠性,主要的方案有以下2種:

1.事務

2.confirm機制

1.1.1、事務

RabbitMQ提供了事務功能,也即在生產者傳送資料之前開啟RabbitMQ事務,然後再傳送訊息,如果訊息沒有成功傳送到RabbitMQ,那麼就丟擲異常,然後進行事務回滾,回滾之後再重新傳送訊息,如果RabbitMQ接收到了訊息,那麼進行事務提交,再開始傳送下一條資料。

優點

保證訊息一定能夠傳送到RabbitMQ中,傳送端不會出現訊息丟失的情況;

缺點

事務機制是阻塞(同步)的,每次傳送訊息必須要等到mq回應之後才能繼續傳送訊息,比較耗費效能,會導致吞吐量降下來

1.1.2、confirm模式

基於事務的特性,作為補償,RabbitMQ新增了訊息確認機制,也即confirm機制。confirm機制和事務機制最大的不同就是事務是同步的,confirm是非同步的,傳送完一個訊息後可以繼續傳送下一個訊息,mq接收到訊息後會非同步回撥介面告知訊息接收結果。生產者開啟confirm模式後,每次傳送的訊息都會分配一個唯一id,如果訊息成功傳送到了mq中,那麼就會返回一個ack訊息,表示訊息接收成功,反之會返回一個nack,告訴你訊息接收失敗,可以進行重試。依據這個機制,我們可以維護每個訊息id的狀態,如果超過一定時間還是沒有接收到mq的回撥,那麼就重發訊息。

1.1.3、confirm模式程式碼演示

其實這塊程式碼在前面幾篇文章的程式碼中有體現過;下面以springboot整合的方式再演示一種

 pom.xml檔案


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
 

application.yml

spring:
  rabbitmq:  #rabbitmq 連線配置
    publisher-confirm-type: correlated # 開啟confirm確認模式
    host: 192.168.0.1
    port: 5672
    username: admin
    password: admin

server:
  port: 8081
實現confirm方法
實現ConfirmCallback介面中的confirm方法,訊息只要被 rabbitmq broker接收到就會觸ConfirmCallback 回撥,ack為true表示訊息傳送成功,ack為false表示訊息傳送失敗
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
    /***
     * @param correlationData 相關配置資訊
     *  @param ack exchange交換機 是否成功收到了訊息。true 成功,false代表失敗
     *  @param cause 失敗原因
     * */
    @Override
    public void confirm(CorrelationData correlationData ,boolean ack ,String cause) {

        if (ack){
            //訊息傳送成功
            System.out.println ("訊息傳送成功到交換機");
        }else{
            System.out.println ("傳送失敗"+cause);
        }
    }
}
定義 Exchange 和 Queue
定義交換機 confirmTestExchange 和佇列 confirm_test_queue ,並將佇列繫結在交換機上。
/**
 * 定義佇列和交換機
 */
@Configuration
public class QueueConfig {
    @Bean(name="confirmTestExchange")
    public FanoutExchange confirmTestExchange(){
        return new FanoutExchange("confirmTestExchange",true,false);
    }
    @Bean(name = "confirmTestQueue")
    public Queue confirmTestQueue(){
        return new Queue("confirm_test_queue",true,false,false);
    }

    @Bean
    public Binding confirmTestFanoutExcangeAndQueue(@Qualifier("confirmTestQueue")Queue queue,@Qualifier("confirmTestExchange") FanoutExchange fanoutExchange){
        return  BindingBuilder.bind(queue).to(fanoutExchange);
    }
}
生產者
@RestController
@RequestMapping(value = "/producer")
@CrossOrigin
public class Producer {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ConfirmCallbackService confirmCallbackService;

    @Autowired
    private ReturnCallbackService returnCallbackService;
    @GetMapping
    public void producer(){

        rabbitTemplate.setConfirmCallback ( confirmCallbackService );
        rabbitTemplate.convertAndSend ( "confirmTestExchange","","測試RabbitTemplate功能" );
    }
}
正確情況,ack返回true,表示投遞成功;下面測試下,發一個正常的截圖和非正常截圖,非正常的截圖只用把生產者的交換機名稱就行

訊息未投遞到queue的退回模式

上面演示了訊息投放到交換機的案例,下面演示一個訊息從 exchange–>queue 投遞失敗則會返回一個 returnCallback的案例;生產端通過實現ReturnCallback介面,啟動訊息失敗返回,訊息路由不到佇列時會觸發該回撥介面

修改yml檔案
spring:
  rabbitmq:  #rabbitmq 連線配置
    publisher-confirm-type: correlated # 開啟confirm確認模式
    publisher-returns: true #開啟退回模式
    host: 192.168.0.1
    port: 5672
    username: admin
    password: admin

server:
  port: 8081
設定投遞失敗的模式
根據前面文章的講解可知如果訊息沒有路由到Queue,則丟棄訊息(預設);但開啟ReturnCallBack後,如果訊息沒有路由到Queue,返回給訊息傳送方ReturnCallBack(開啟後)
rabbitTemplate.setMandatory(true);
實現returnedMessage方法
啟動訊息失敗返回,訊息路由不到佇列時會觸發該回撥介面
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {
    /**
     *
     * @param message 訊息物件
     * @param i 錯誤碼
     * @param s 錯誤資訊
     * @param s1 交換機
     * @param s2 路由鍵
     */
    @Override
    public void returnedMessage(Message message ,int i ,String s ,String s1 ,String s2) {
        System.out.println("訊息物件:" + message);
        System.out.println("錯誤碼:" + i);
        System.out.println("錯誤資訊:" + s);
        System.out.println("訊息使用的交換器:" + s1);
        System.out.println("訊息使用的路由key:" + s2);
        //業務程式碼處理
    }
}

yml配置

spring:
  rabbitmq:  #rabbitmq 連線配置
    publisher-confirm-type: correlated # 開啟confirm確認模式
    publisher-returns: true #開啟退回模式
    host: 124.71.33.75
    port: 5672
    username: admin
    password: ghy20200707rabbitmq

server:
  port: 8081
public void producerLose(){

        /**
         *確保訊息傳送失敗後可以重新返回到佇列中
         */
        rabbitTemplate.setMandatory(true);

        /**
         * 訊息投遞確認模式
         */
        rabbitTemplate.setConfirmCallback(confirmCallbackService);
        /**
         * 訊息投遞到佇列失敗回撥處理
         */
        rabbitTemplate.setReturnCallback(returnCallbackService);
        CorrelationData correlationData = new CorrelationData("id_"+System.currentTimeMillis()+"");
        //傳送訊息
        rabbitTemplate.convertAndSend("directExchange", "RabbitTemplate","測試RabbitTemplate功能" ,correlationData);
    }

測試介面

 

 1.2、消費者丟失訊息

其實在生產者和消費者中間,rabbitmq也是會丟失訊息的,解決方案就是持久化儲存,這個方案在前面有講過;所以在這裡就跳過;下面直接說訊息確認機制ack,ack指Acknowledge確認。 表示消費端收到訊息後的確認方式

消費端訊息的確認分為:自動確認(預設)、手動確認、不確認
  • AcknowledgeMode.NONE:不確認
  • AcknowledgeMode.AUTO:自動確認
  • AcknowledgeMode.MANUAL:手動確認
其中自動確認是指,當訊息一旦被Consumer接收到,則自動確認收到,並將相應 message 從RabbitMQ 的訊息 快取中移除。但是在實際業務處理中,很可能訊息接收到,業務處理出現異常,那麼該訊息就會丟失。如果設定了手動確認方式,則需要在業務處理成功後,呼叫channel.basicAck(),手動簽收,如果出現異常,則呼叫channel.basicNack()方法,讓其自動重新傳送訊息。
yml配置
spring:
  rabbitmq:  #rabbitmq 連線配置
    publisher-confirm-type: correlated # 開啟confirm確認模式
    publisher-returns: true #開啟退回模式
    host: 124.71.33.75
    port: 5672
    username: admin
    password: ghy20200707rabbitmq
    listener:
      simple:
        acknowledge-mode: manual #手動確認

server:
  port: 8081
確認配置
/**
 * 消費者訊息確認機制
 */
@Component
@RabbitListener(queues = "confirm_test_queue")
public class ReceiverMessage {
    @RabbitHandler
    public void processHandler(String msg,Channel channel,Message message) throws IOException { 
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.println("訊息內容" + new String(message.getBody())); 
            //TODO 具體業務邏輯 
            // 手動簽收[引數1:訊息投遞序號,引數2:批量簽收] 
            channel.basicAck(deliveryTag, true); 
        } catch (Exception e) { 
            //拒絕簽收[引數1:訊息投遞序號,引數2:批量拒絕,引數3:是否重新加入佇列] 
            channel.basicNack(deliveryTag, true, true); 
        } 
    }
}
channel.basicNack 方法與 channel.basicReject 方法區別在於basicNack可以批量拒絕多條訊息,而basicReject一次只能拒絕一條訊息。測試效果如下:

 

 要想測試異常很簡單,在程式碼加一個報錯語句就可以測試了,我這裡就不搞事了;

二、消費端限流

假設一個場景,首先,在 Rabbitmq 伺服器積壓了有上萬條未處理的訊息,這時隨便開啟一個消費者客戶端,會出現這樣情況: 巨量的訊息瞬間全部推送過來,但是單個客戶端無法同時處理這麼多資料!當資料量特別大的時候,如果對生產端限流肯定是不科學的,因為有時候併發量就是特別大,有時候併發量又特別少,這是使用者的行為,使用者的行為是不可控的。所以正確的處理方案應該是對消費端限流,用於保持消費端的穩定,當訊息數量激增的時候很有可能造成資源耗盡,以及影響服務的效能,導致系統的卡頓甚至直接崩潰。

2.1、TTL

Time To Live,訊息過期時間設定
宣告佇列時,指定即可
TTL:過期時間
1. 佇列統一過期
2. 訊息單獨過期
如果設定了訊息的過期時間,也設定了佇列的過期時間,它以時間短的為準。佇列過期後,會將佇列所有訊息全部移除;訊息過期後,只有訊息在佇列頂端,才會判斷其是否過期(移除掉)

三、死信佇列

死信佇列,當訊息成為Dead message後,可以被重新傳送到另一個交換機,這個交換機就是DLX;關於死信佇列的演示程式碼在第一篇中有上傳過;這裡就不再演示了;
 
 

 

 

訊息成為死信的三種情況:
  1. 佇列訊息長度到達限制;
  2. 消費者拒接消費訊息,basicNack/basicReject,並且不把訊息重新放入原目標佇列,requeue=false;
  3.  原佇列存在訊息過期設定,訊息到達超時時間未被消費;
 
佇列繫結死信交換機:
給佇列設定引數: x-dead-letter-exchange 和 x-dead-letter-routing-key也就是說此時Queue作為"生產者"

 

 

四、延遲佇列

延遲佇列,即訊息進入佇列後不會立即被消費,只有到達指定時間後,才會被消費,最常見的業務就是定單服務,例如:一個定單下單後如果30分鐘內沒有支援,就要取消定單,回回滾庫存。
其實在RabbitMQ中並未提供延遲佇列功能 ,但是有替代方案,他的替代方案就是TTL+死信佇列組合實現延遲佇列的效果 ;
設定佇列過期時間30分鐘,當30分鐘過後,訊息未被消費,進入死信佇列,路由到指定佇列,呼叫庫存系統,判斷訂單狀態。

相關文章