rabbitmq消息確認機制及死信隊列的使用

關於rabbitmq的基本概念和相關的理論這裏就不做過多介紹了,在之前的篇幅中有過相應的介紹,也可以查詢一些資料詳細瞭解一下rabbitmq的基礎知識,下面要介紹的點主要包括兩個方面,

1、rabbitmq的消息確認機制;
2、rabbitmq的延時隊列,也稱作爲死信隊列的一些研究心得分享

爲什麼會產生消息確認這個概念呢?其實rabbitmq的模式是我們熟悉的典型的觀察者模式的具體實現,或者說是監聽器模式可以,生產者往隊列投遞了一條消息,消費者從隊列取出消息消費,這是很好理解的;

但是在rabbitmq中引入了exchange,即交換機這個概念,我們可以理解爲一個消息的中轉站或者是消息的分發集散中心,在這裏,rabbitmq相比kafka或者activemq提供了更爲高級的功能,就是支持消息的精確路由,消息的模糊匹配等功能,這樣一來,對於整個消息從生產者到消費者最終消費到這條消息,中間的鏈路比起單純的鏈路,生產者 —>隊列—>消費者,中間多了一些環節,這也就造成了消息能否最終發送並被消費成功的不確定性,正是這種不確定性使得我們在使用的時候會關注消息到每一步的時候的狀態,也就產生了消息的確認機制;

下面,我們先看一張關於rabbitmq從生產者發送消息到exchange然後到指定隊列的整個流程示意圖,如下所示,

通過這張示意圖,相信大家可以大致瞭解了上述解釋的意思所在,也可以看出來,消息需要確認的地方無非有3處,

1、消息是否能找到對應的exchange,即生產者的消息是否能夠準確投遞到指定的exchange中,如果找不到,則會被退回;

2、消息投遞到exhange成功,但是沒有找到合適的隊列,即消息無法被路由到指定的queue中去,導致消息無法被投遞和消費,也會被退回;

3、最後,消息被某個消費者消費,但是沒有確認

退回這個詞可以認爲是程序中處理未被確認的消息的一致機制,或者說一種處理方式,在rabbitmq中可以是退回這條未被確認的消息,或者是丟棄掉可以根據業務場景具體使用;

既然我們清楚了消息需要確認的地方,下面我們通過代碼來模擬一下這個場景,加深理解一下其內涵,

爲演示和使用方便,這裏我們使用springboot整合rabbitmq做項目演示,項目結構大家可以自己指定,網上關於springboot整合rabbitmq的demo也很多,這裏我主要貼上關鍵代碼,大家也可以參考我之前的關於springboot整合rabbitmq的案例,

首先貼上配置文件,配置文件裏的參數都有註釋,

 

 


server.port=8082

#rabbitmq的相關配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/test

spring.rabbitmq.connection-timeout=2000ms

#生產者確認消息  confirmListener
spring.rabbitmq.publisher-confirms=true
#消息未被消費則原封不動返回,不被處理  returnListener  和 mandatory 配合使用
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-mandatory=true

#定義消費者最多同時消費10個消息
#spring.rabbitmq.listener.simple.prefetch=10
spring.rabbitmq.listener.simple.concurrency=1
spring.rabbitmq.listener.simple.max-concurrency=5
#設置手動確認消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#支持消息重試
spring.rabbitmq.listener.simple.retry.enabled=true


1、從controller層開始,模擬發送一條對象消息,

    

/**
     * 發送對象消息
     * @return
     */
    @GetMapping("/sendEmployeeMessage")
    @ResponseBody
    public String sendEmployeeMessage(){
        Employee employee = new Employee();
        employee.setAge(23);
        employee.setEmpno("007");
        employee.setName("jike");
        messageProducer.sendMessage(employee);
        return "success";
    }



2、我們來看 上面的sendMessage 這個方法,在這個方法裏面主要做了兩件事,一個是發送對象消息,然後就是在發送過程中添加了消息確認的回調函數,要注意的是這裏的回調函數目前跟消費者的通道是沒有任何關係的,即消息最終能否成功發送到exchange上以及exchange能否將消息路由到指定的隊列,

@Component
public class MessageProducer {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    //消息確認機制,如果消息已經發出,但是rabbitmq並沒有迴應或者是拒絕接收消息了呢?就可以通過回調函數的方式將原因打印出來
    RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
        
        public void confirm(CorrelationData correlationData, boolean isack, String cause) {
            System.out.println("本次消息的唯一標識是:" + correlationData);
            System.out.println("是否存在消息拒絕接收?" + isack);
            if(isack == false){
                System.out.println("消息拒絕接收的原因是:" + cause);
            }else{
                System.out.println("消息發送成功");
            }
        }
    };
    
    //有關消息被退回來的具體描述消息
    RabbitTemplate.ReturnCallback returnCallback = new ReturnCallback() {
        
        @Override    
        public void returnedMessage(Message message, 
                                    int replyCode, 
                                    String desc, 
                                    String exchangeName, 
                                    String routeKey) {
            System.out.println("err code :" + replyCode);
            System.out.println("錯誤消息的描述 :" + desc);
            System.out.println("錯誤的交換機是 :" + exchangeName);
            System.out.println("錯誤的路右鍵是 :" + routeKey);
            
        }
    };
    
    //發送對象消息時
    /**
     * CorrelationData  標識消息唯一性的主體對象,可以自己設定相關的參數,方便後續對某條消息做精確的定位
     * confirmCallback  消息投遞到rabbitmq是否成功的回調函數,如果不成功,我們可以在該回調函數中做相關的處理
     * returnCallback   消息被退回的回調函數
     * @param employee
     */
    public void sendMessage(Employee employee){
        CorrelationData cData = new CorrelationData(employee.getEmpno() + "-" + new Date().getTime());
        rabbitTemplate.setConfirmCallback(confirmCallback);
        rabbitTemplate.setReturnCallback(returnCallback);
        rabbitTemplate.convertAndSend("springboot-exchange", "hr.employee",employee,cData);
    }
    
}



3、消費者一端代碼,實際開發中,消費端的項目可能會在其他的工程中,這個並不會影響使用,

@Component
public class HandlerOrderMessage {
    
    /**
     * 單純接收map的類型的消息
     * @param message
     */
    /*@RabbitListener(queues="java_queue")
    @RabbitHandler
    public void handleOrder(Map<String, Object> message){
        System.out.println("收到了訂單消息 :" + message.get("name"));
    }*/
    
    @RabbitListener(queues="java_queue")
    @RabbitHandler
    public void handleEmployeeMsg(@Payload Employee employee,Channel channel,
                                  @Headers Map<String, Object> headers){
        
        System.out.println("消費者開始接收員工消息 =================");
        System.out.println(employee.getName());
        Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
            channel.basicAck(tag, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
}



4、在我們啓動項目之前,還有一個很重要的配置,想必大家也很快就想到了,就是config的配置文件,在整合rabbitmq中,我們需要提前準備一個bean的class,用於在項目啓動時候初始化相關的隊列以及設置隊列和exchange進行綁定的相關代碼,這裏爲了模擬出效果,我們先不提供這個配置類,而且在rabbitmq控制檯也不提前創建隊列看看會有什麼效果呢?啓動項目後,瀏覽器輸入,
http://localhost:8082/map/sendEmployeeMessage,我們看一下控制檯的輸出,


可以肯定的是這條消息發送失敗了,失敗的原因是什麼呢?我們在看看後面的日誌,意思是在/test這個虛擬的virtualhost下面沒有找到這個交換機

從這裏我們可以印證示意圖中所說的第一點,然後我們手動創建上這個exchange,但是並不做springboot-exchange和隊列的綁定,然後再次訪問,

http://localhost:8082/map/sendEmployeeMessage,看看控制檯的答應結果,

這裏消息走到了returnCallback 這個回調函數裏面,意思就是消息被退回來了,按照上面的分析就是消息發送到了exchange,但是exchange沒有找到合適的隊列進行投遞,因此被退回了,注意的是

"發送成功"是消息發送到exchange這個裏面發送成功了,這個發送成功的回到函數是,confirmCallback,而是否能夠發送到隊列成功的回調函數是,returnCallback,注意區分開

基本上到這裏,就把消息確認機制的基本原理講完了,實際工作中,我們可以繼續進行後續的處理,比如消息發送失敗了該如何處理,如何第一時間反饋到開發人員進行問題的排查,都可以在回調函數裏面做一些處理的,

接下來說說第二個問題,就是rabbitmq比較特殊的一種隊列,叫做死信隊列或者說延時隊列,顧名思義,就是對於那些超時未消費的消息,或者是業務的需要處理一些需要延時消費的消息的一種特殊處理機制,

延時隊列的使用場景:
1.訂單業務:在電商中,用戶下單後30分鐘後未付款則取消訂單。

2.短信通知:用戶下單並付款後,1分鐘後發短信給用戶。

延時隊列實現思路
特性一:Time To Live(TTL)

RabbitMQ可以針對Queue設置x-expires 或者 針對Message設置 x-message-ttl,來控制消息的生存時間,如果超時(兩者同時設置以最先到期的時間爲準),則消息變爲dead letter(死信)
RabbitMQ針對隊列中的消息過期時間有兩種方法可以設置。
A: 通過隊列屬性設置,隊列中所有消息都有相同的過期時間。
B: 對消息進行單獨設置,每條消息TTL可以不同。

如果同時使用,則消息的過期時間以兩者之間TTL較小的那個數值爲準。消息在隊列的生存時間一旦超過設置的TTL值,就成爲dead letter

特性二:Dead Letter Exchanges(DLX)

RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可選)兩個參數,如果隊列內出現了dead letter,則按照這兩個參數重新路由轉發到指定的隊列。
x-dead-letter-exchange:出現dead letter之後將dead letter重新發送到指定exchange
x-dead-letter-routing-key:出現dead letter之後將dead letter重新按照指定的routing-key發送
隊列出現dead letter的情況有:
消息或者隊列的TTL過期
隊列達到最大長度

消息被消費端拒絕(basic.reject or basic.nack)並且requeue=false

死信隊列 聽上去像 消息“死”了 其實也有點這個意思,死信隊列 是 當消息在一個隊列 因爲下列原因:

消息被拒絕(basic.reject/ basic.nack)並且不再重新投遞 requeue=false
消息超期 (rabbitmq Time-To-Live -> messageProperties.setExpiration())
隊列超載
變成了 “死信” 後 被重新投遞(publish)到另一個Exchange 該Exchange 就是DLX 然後該Exchange 根據綁定規則 轉發到對應的 隊列上 監聽該隊列 就可以重新消費 說白了 就是 沒有被消費的消息 換個地方重新被消費


下面我們模擬一個死信隊列的應用場景 消息延時處理

1、死信隊列的相關配置類,

package com.acong.rabbitconfig;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.rabbitmq.client.AMQP.Exchange;

import java.util.HashMap;
import java.util.Map;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;

/**
 * 死信隊列相關配置
 * 
 * @author asus
 *
 */
@Configuration
public class DeadQueueConfig {

    /**
     * 死信隊列 交換機標識符
     */
    private static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";

    /**
     * 死信隊列交換機綁定鍵標識符
     */
    private static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";

    /**
     * 死信隊列跟交換機類型沒有關係 不一定爲directExchange 不影響該類型交換機的特性.
     */
    @Bean("deadLetterExchange")
    public DirectExchange deadLetterExchange() {
        // return (DirectExchange)
        // ExchangeBuilder.directExchange("DL_EXCHANGE").durable(true).build();
        return new DirectExchange("DL_EXCHANGE", true, false);
    }

    /**
     * 聲明一個死信隊列. x-dead-letter-exchange 對應 死信交換機 x-dead-letter-routing-key 對應
     * 死信隊列
     */
    @Bean("deadLetterQueue")
    public Queue deadLetterQueue() {
        Map<String, Object> args = new HashMap<>(2);
        // x-dead-letter-exchange 聲明 死信交換機
        args.put(DEAD_LETTER_QUEUE_KEY, "DL_EXCHANGE");
        // x-dead-letter-routing-key 聲明 死信路由鍵
        args.put(DEAD_LETTER_ROUTING_KEY, "KEY_R");
        return QueueBuilder.durable("DL_QUEUE").withArguments(args).build();
    }

    /**
     * 定義死信隊列轉發隊列.
     * 
     * @return the queue
     */
    @Bean("redirectQueue")
    public Queue redirectQueue() {
        return QueueBuilder.durable("REDIRECT_QUEUE").build();
    }

    /**
     * 死信路由通過 DL_KEY 綁定鍵綁定到死信隊列上.
     *
     * @return the binding
     */
    @Bean
    public Binding deadLetterBinding() {
        return new Binding("DL_QUEUE", Binding.DestinationType.QUEUE, "DL_EXCHANGE", "DL_KEY", null);
    }

    /**
     * 死信路由通過 KEY_R 綁定鍵綁定到死信隊列上.
     * 
     * @return the binding
     */
    @Bean
    public Binding redirectBinding() {
        return new Binding("REDIRECT_QUEUE", Binding.DestinationType.QUEUE, "DL_EXCHANGE", "KEY_R", null);
    }
}


說明:
deadLetterExchange()聲明瞭一個Direct 類型的Exchange (死信隊列跟交換機沒有關係)

deadLetterQueue() 聲明瞭一個隊列 這個隊列 跟前面我們聲明的隊列不一樣 注入了 Map<String,Object> 參數 下面的概念非常重要

x-dead-letter-exchange 來標識一個交換機 x-dead-letter-routing-key 來標識一個綁定鍵(RoutingKey) 這個綁定鍵 是分配給 標識的交換機的 如果沒有特殊指定 聲明隊列的原routingkey , 如果有隊列通過此綁定鍵 綁定到交換機 那麼死信會被該交換機轉發到 該隊列上 通過監聽 可對消息進行消費

可以打個比方 這個是爲主力隊員 設置了一個替補 如果主力 “死”了 他的活 替補接手 這樣更好理解

deadLetterBinding() 對這個帶參隊列 進行了 和交換機的規則綁定 等下 消費者 先把消息通過交換機投遞到該隊列中去 然後製造條件發生“死信”

redirectBinding() 我們需要給標識的交換機 以及對其指定的routingkey 來綁定一個所謂的“替補”隊列 用來監聽

流程具體是 消息投遞到 DL_QUEUE 10秒後消息過期 生成死信 然後轉發到 REDIRECT_QUEUE 通過對其的監聽 來消費消息

2、SendController 增加消費發送接口

@RequestMapping("/dead")
@Controller
public class SendController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 測試死信隊列. http://localhost:8082/dead/deadLetter?p=11234
     */
    @RequestMapping("/deadLetter")
    @ResponseBody
    public ResponseEntity deadLetter(String p) {

        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());

        // 聲明消息處理器 這個對消息進行處理 可以設置一些參數 對消息進行一些定製化處理 我們這裏 來設置消息的編碼 以及消息的過期時間
        // 因爲在.net 以及其他版本過期時間不一致 這裏的時間毫秒值 爲字符串
        MessagePostProcessor messagePostProcessor = message -> {
            MessageProperties messageProperties = message.getMessageProperties();
            // 設置編碼
            messageProperties.setContentEncoding("utf-8");
            // 設置過期時間10*1000毫秒
            messageProperties.setExpiration("10000");
            return message;
        };
        // 向DL_QUEUE 發送消息 10*1000毫秒後過期 形成死信,具體的時間可以根據自己的業務指定
        rabbitTemplate.convertAndSend("DL_EXCHANGE", "DL_KEY", p, messagePostProcessor, correlationData);
        return ResponseEntity.ok();
    }

}


3、監聽死信的替補隊列,REDIRECT_QUEUE ,即死信路由到的隊列,


@Component
public class Consumer {
    
    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);
    
    /**
     * 監聽替補隊列 來驗證死信.
     *
     * @param message the message
     * @param channel the channel
     * @throws IOException the io exception  這裏異常需要處理
     */
    @RabbitListener(queues = "REDIRECT_QUEUE")
    @RabbitHandler
    public void redirect(Message message, Channel channel) throws IOException {
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        logger.info("dead message  10s 後 消費消息 :" + new String (message.getBody()));
    }
    
}

下面啓動項目,瀏覽器輸入,
http://localhost:8082/dead/deadLetter?p=11234,
可以看到大概等了10秒,消費者收到了死信的消息,
這也驗證了我們上面對於死信隊列的解釋說明,我這裏使用的是DLX的方式實現的,大家也可以思考一下駛入使用TTL的方式實現,

基本上到這裏,本篇的整合就結束了,希望對看到的小夥伴有所幫助,大家也可以在此基礎上進行更加深入的研究和探討,最後感謝觀看!

附上源碼下載地址:https://download.csdn.net/download/zhangcongyi420/11186779
————————————————
版權聲明:本文爲CSDN博主「神祕的蔥」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/zhangcongyi420/article/details/90319157

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