RabbitMQ-如何保證訊息不丟失

Bob_F發表於2024-06-12

RabbitMQ常用於 非同步傳送,mysql,redis,es之間的資料同步 ,分散式事務,削峰填谷等.....

在微服務中,rabbitmq是我們經常用到的訊息中介軟體。它能夠非同步的在各個業務之中進行訊息的接受和傳送,那麼如何保證rabbitmq的訊息不丟失就顯得尤為重要。

首先要分析問題,我們就要明確rabbitmq在什麼時候可能會出現訊息丟失的情況呢?

我們直接說結果

RabbitMQ在每個階段都有可能使訊息發生丟失

我們在這裡把他們簡單歸結為三個層面

層面一 :生產者傳送訊息沒有到達交換機或者沒有到達繫結的佇列。

層面二:RabbitMQ當機可能導致的訊息的丟失。

層面三:消費者當機導致訊息丟失。

層面一的解決方法常見的是

1.生產者確認機制

RabbitMQ提供了publisher confirm機制來避免訊息傳送到Mq的過程中丟失,訊息傳送到Mq以後,會返回一個結果給傳送者,表示訊息的傳送成功。

情況一:傳送成功 生產者正常傳送訊息到佇列之後會返回一個publish-confirm ack 這個意思是告訴生產者已經接收到訊息了。

情況二:傳送失敗 這裡的傳送失敗有兩種,一種是生產者傳送到交換機失敗 此時返回 publish-confirm nack 。第二種是生產者傳送到佇列失敗 返回 publish-return ack。

開啟生產者確認機制的程式碼如下 ,在生產者的配置檔案中加入以下配置

spring:
  rabbitmq:
    publisher-confirm-type: correlated #開啟生產者確認機制
    publisher-returns: true

這裡的

publisher-confirm-type:有三種模式可以選擇:
第一種是none:代表關閉confirm機制

第二種是 simple:表示同步阻塞並等待mq的回執訊息,即傳送完訊息後不能幹其他的事情,只能等待mq的回執,很顯然這樣效率很低。

第三種是correlated:MQ非同步回撥方式返回回執訊息,即生產者傳送完訊息後可以幹其他的事情,直到接收到mq的回執。很明顯這種效率要優於第二種。

配置return callback的程式碼如下,每個RabbitTemplate只能配置一個 程式碼如下

package com.itheima.publisher.com.it.heima.config;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
 
/**
 * @Auther: Ruib
 * @Date: 2024/1/13 10:34
 * @Description:
 */
@Slf4j
@Configuration
public class MqConfirmConfig implements ApplicationContextAware {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        //配置回撥
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                log.debug("收到訊息return的callback,  {},{},{},{},{}",
                        returnedMessage.getExchange(),
                        returnedMessage.getRoutingKey(),
                        returnedMessage.getMessage(),
                        returnedMessage.getReplyCode(),
                        returnedMessage.getReplyText());
            }
        });
    }
}

Confirm Callback需要每次發訊息的時候都要配置(要制定發訊息的id方便回執的時候直到是誰發的訊息)這裡寫一個測試類方便大家看。

 @Test
    void testConfirmCallback() throws InterruptedException {
        //建立cd 引數為每次傳送訊息的id
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //新增confirmCallBack
        correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
            @Override
            public void onFailure(Throwable ex) {
                //這種情況一般是執行出現bug,一般不會發生。
                log.error("訊息回撥失敗",ex);
            }
 
            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                log.debug("收到confirm callback 回執");
                if (result.isAck()){
                    //訊息傳送成功
                    log.debug("訊息傳送成功收到ack");
                }else {
                    //訊息傳送失敗
                    log.debug("訊息傳送失敗收到nack,原因:{}",result.getReason());
                    //TODO 重發訊息等業務
                }
            }
        });
 
        rabbitTemplate.convertAndSend("amqp.test","amqptest","hello qjc",correlationData);
 
        Thread.sleep(2000);
    }

那麼我們如何解決這個問題呢
方案一:重發訊息

方案二:記錄日誌

方案三:儲存到資料庫中定時傳送,傳送成功後刪除表中的資料。

方案四:交給人工處理。

~生產者確認機制需要額外的網路和系統的資源開銷,儘量不要使用。

~如果業務需要,那麼無需開啟publisher-return機制,因為一般路由失敗都是自己業務的原因。

~對於nack訊息可以有限次數的重試,依然失敗則記錄異常訊息。

層面二的解決方法常見的是

2.訊息持久化

由於mq是基於記憶體儲存訊息的,那麼在mq服務當機等一些情況下可能導致訊息的丟失。同時記憶體空間有限,當消費者出現故障或者處理過慢,會導致訊息積壓,mq會對訊息做遷移(page out 寫入磁碟)從而引發mq阻塞。我們將訊息儲存在磁碟上就避免了這個問題。

一 :持久化交換機。

這裡要選擇Durable,因為Transient是臨時交換機,當mq當機後會消失。

程式碼展示

 @Bean
    public DirectExchange simpleExchange(){
        //分別是三個引數 交換機名稱 是否持久化 當沒有佇列繫結時是否自動刪除
        return new DirectExchange("qjc.exchange",true,false);
    }

二 :持久化佇列。

這個與交換機類似,在此不做贅述。

程式碼展示

@Bean
    public Queue simpleQueue(){
        //springamqp在使用QueueBuilder來建立佇列的時候,預設就是持久化的
        return QueueBuilder.durable("qjc.queue").build();
    }

三 :持久化訊息。

這裡選擇delivery mode 選擇2 ,1是不持久的。

程式碼展示

 Message message = MessageBuilder.withBody("hello".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .build();

如果不選擇持久化佇列,交換機,訊息的話我們還有另一種方案
Lazy Queue(惰性佇列)

惰性佇列的特徵如下

~接受到訊息的時候直接存入磁碟而非記憶體(記憶體中只保留最近的訊息)

~消費者需要訊息的時候才會從磁碟中取出資料載入到記憶體

~支援數百萬條的訊息儲存

在mq3.12版本後,所有的佇列都是Lazy Queue模式,無法更改。

如果各位小夥伴的版本低於3.12那我這裡提供了兩種方式建立惰性佇列

或用註解宣告

    @RabbitListener(queuesToDeclare = @Queue(
            name = "lazy.queue",
            durable = "true",
            arguments = @Argument(name = "x-queue-mode",value = "lazy")
    ))
    public void listenLazyQueue(String msg){
        log.debug("接收到lazyqueue的訊息" + msg);
    }

層面三的解決方法常見的是

3.消費者確認機制

RabbitMQ支援消費者確認機制,即:當消費者處理訊息後可以向mq傳送ack回執,mq收到訊息後會在佇列中刪除該訊息。

SpringAMQP已經實現了訊息確認的功能,並且允許我們透過配置檔案選擇ack的處理方式,有三種方式。

- none: 不處理。即訊息投遞給消費者後立刻ack,訊息會立刻從MQ刪除。非常不安全,不建議使用
- manual: 手動模式。需要自己在業務程式碼中呼叫api,傳送ack或reject,存在業務入侵,但更靈活
- auto: 自動模式。SpringAMQP利用AOP對我們的訊息處理邏輯做了環繞增強,當業務正常執行時則自動返回ack.
當業務出現異常時,根據異常判斷返回不同結果:
- 如果是業務異常,會自動返回nack
- 如果是訊息處理或校驗異常,自動返回reject

注意我們需要再消費者的配置檔案中加入引數

總結,上述‘生產者確認機制’、‘訊息持久化’、‘消費者確認機制’就是mq保證訊息不丟失的一些方式和解決方案。

參考連結:

https://blog.csdn.net/qq_63945982/article/details/135832721

相關文章