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