RabbitMQ SpringBoot 延時隊列 ( 存活時間TTL 和 死信隊列DLX ) 應用

一.延時隊列使用場景

在很多的業務場景中,延時隊列可以實現很多功能,此類業務中,一般上是非實時的,需要延遲處理的,需要進行重試補償的。

  1. 訂單超時關閉:在支付場景中,一般上訂單在創建後30分鐘或1小時內未支付的,會自動取消訂單。
  2. 短信或者郵件通知:在一些註冊或者下單業務時,需要在1分鐘或者特定時間後進行短信或者郵件發送相關資料的。
  3. 重試場景:比如消息通知,在第一次通知出現異常時,會在隔幾分鐘之後進行再次重試發送。

二.RabbitMQ實現延時隊列

本身在RabbitMQ中是未直接提供延時隊列功能的,但可以使用 TTL(Time-To-Live,存活時間) 和 DLX(Dead-Letter-Exchange,死信隊列交換機) 的特性實現延時隊列的功能。

三.存活時間 TTL(Time-To-Live )

RabbitMQ中可以對隊列和消息分別設置TTL,TTL表明了一條消息可在隊列中存活的最大時間。當某條消息被設置了TTL或者當某條消息進入了設置了TTL的隊列時,這條消息會在TTL時間後死亡成爲Dead Letter。如果既配置了消息的TTL,又配置了隊列的TTL,那麼較小的那個值會被取用。

四.死信交換 DLX(Dead Letter Exchanges )

設置了TTL的消息或隊列最終會成爲Dead Letter,當消息在一個隊列中變成死信之後,它能被重新發送到另一個交換機中,這個交換機就是DLX,綁定此DLX的隊列就是死信隊列。

一個消息變成死信一般上是由於以下幾種情況;

  1. 消息被拒絕
  2. 消息過期
  3. 隊列達到了最大長度。

所以,通過TTL和DLX的特性可以模擬實現延時隊列的功能。當隊列中的消息超時成爲死信後,會把消息死信重新發送到配置好的交換機中,然後分發到真實的消費隊列。故簡單來說,我們可以創建2個隊列,一個隊列用於發送消息,一個隊列用於消息過期後的轉發的目標隊列。

五. SpringBoot集成RabbitMQ實現延時隊列實戰

下面是死信隊列在創建、綁定、生產消息、消費消息過程的結構流程圖:
在這裏插入圖片描述
圖中問題的答案爲:當入死信隊列的消息TTL一到,它自然而然的將被路由到 死信交換機綁定的隊列 中被真正消費處理!!!

六.rabbitmq-produce的改動

項目使用上一篇中的項目 rabbitmq-produce、rabbitmq-consumer

2.1 在rabbitmq-produce中,新增一個DealRabbitConfig 配置類

DealRabbitConfig 的代碼如下:

package com.example.rabbitmqproduce.config;


import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;

/**
 * 死信隊列
 */
@Configuration
public class DealRabbitConfig {

    private Logger logger = LoggerFactory.getLogger(DealRabbitConfig.class);


    //死信隊列 模型
    @Bean
    public Queue dealQueue() {
        Map<String, Object> argsMap = Maps.newHashMap();
        //當變成死信隊列時,會轉發至 路由爲x-dead-letter-exchange及x-dead-letter-routing-key的隊列中
        argsMap.put("x-dead-letter-exchange", "deal.exchange");
        argsMap.put("x-dead-letter-routing-key", "deal.routing.key");
        //過期時間(單位:毫秒),當過期後 會變成死信隊列,之後進行轉發
        //TTL 這邊可以不填,可以動態設置,通過MessageProperies屬性設定
        argsMap.put("x-message-ttl", 10000);
        return new Queue("deal.queue", true, false, false, argsMap);
    }


    //生產端的交換機
    @Bean
    public TopicExchange produceExchange(){
        return new TopicExchange("produce.exchange",true,false);
    }


    //生產端的交換機和路由key 綁定 死信隊列
    @Bean
    public Binding produceToDeal(){
        return BindingBuilder.bind(dealQueue()).to(produceExchange()).with("produce.routing.key");
    }



    //消費端的隊列
    @Bean
    public Queue consumerQueue(){
        return new Queue("consumer.queue",true);
    }


    //死信交換機
    @Bean
    public TopicExchange deadExchange(){
        return new TopicExchange("deal.exchange",true,false);
    }


    //死信交換機+死信路由key->消費端的隊列
    @Bean
    public Binding dealToConsumer(){
        return BindingBuilder.bind(consumerQueue()).to(deadExchange()).with("deal.routing.key");
    }

}

6.2 新建消息生產者RabbitDealProduce

RabbitDealProduce 的代碼如下:

package com.example.rabbitmqproduce.produce;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 死信隊列   消息生產者
 * @Component 注入到Spring容器中
 */
@Component
public class RabbitDealProduce {

    //注入一個AmqpTemplate來發布消息
    @Autowired
    private AmqpTemplate rabbitTemplate;

    private Logger logger = LoggerFactory.getLogger(RabbitDealProduce.class);

    /**
     * topicA 發送消息
     */
    public void send() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "hello!死信隊列";
        String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        Map<String,Object> map=new HashMap<>();
        map.put("messageId",messageId);
        map.put("messageData",messageData);
        map.put("createTime",createTime);
        logger.info("發送的內容 : " + map.toString());
        rabbitTemplate.convertAndSend("produce.exchange", "produce.routing.key", map);
    }

}

6.3 寫個測試的TestController類

代碼如下:

package com.example.rabbitmqproduce.controller;


import com.example.rabbitmqproduce.produce.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("TestController")
public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @Autowired
    private RabbitMqProduce rabbitMqProduce;

    @Autowired
    private DirectExchangeProduce directExchangeProduce;

    @Autowired
    private FanoutExchangeProduce fanoutExchangeProduce;

    @Autowired
    private TopicExchangeProduce topicExchangeProduce;

    @Autowired
    private RabbitAckProduce rabbitAckProduce;

    @Autowired
    private RabbitDealProduce rabbitDealProduce;

    /**
     * 測試基本消息模型(簡單隊列)
     */
    @RequestMapping(value = "/testSimpleQueue", method = RequestMethod.POST)
    public void testSimpleQueue() {
        logger.info("測試基本消息模型(簡單隊列)SimpleQueue---開始");
        for (int i = 0; i < 10; i++) {
            rabbitMqProduce.sendMessage();
        }
        logger.info("測試基本消息模型(簡單隊列)SimpleQueue---結束");
    }


    /**
     * 測試 Direct-exchange模式
     */
    @RequestMapping(value = "/directExchangeTest", method = RequestMethod.POST)
    public void directExchangeTest() {
        logger.info("測試 Direct-exchange模式 隊列名爲directQueue---開始");
        for (int i = 0; i < 10; i++) {
            directExchangeProduce.sendMessage();
        }
        logger.info("測試 Direct-exchange模式 隊列名爲directQueue---結束");
    }


    /**
     * 測試 Fanout-exchange模式
     */
    @RequestMapping(value = "/fanoutExchangeTest", method = RequestMethod.POST)
    public void fanoutExchangeTest() {
        logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---開始");
        fanoutExchangeProduce.sendMessage();
        logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---結束");
    }


    /**
     * 測試 Topic-exchange模式   topicA 和 topicB
     */
    @RequestMapping(value = "/topictExchangeTest", method = RequestMethod.POST)
    public void topictExchangeTest() {
        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---開始");
        topicExchangeProduce.sendMessageTopicA();
        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---結束");

        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---開始");
        topicExchangeProduce.sendMessageTopicB();
        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---結束");
    }


    /**
     * 測試 ack模式
     */
    @RequestMapping(value = "/ackTest", method = RequestMethod.POST)
    public void ackTest() {
        logger.info("測試 ack 隊列名爲ackQueue---開始");
        rabbitAckProduce.sendMessage();
        logger.info("測試 ack 隊列名爲ackQueue---結束");
    }


    /**
     * 測試 死信隊列
     */
    @RequestMapping(value = "/dealTest", method = RequestMethod.POST)
    public void dealTest() {
        logger.info("測試 死信隊列---開始");
        rabbitDealProduce.send();
        logger.info("測試 死信隊列---結束");
    }

}

七.rabbitmq-consumer的改動

7.1 新建消息消費者RabbitDealConsumer類

RabbitDealConsumer代碼如下:

package com.example.rabbitmqconsumer.consumer;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 死信隊列 的消息消費者
 * @RabbitListener(queues = "consumer.queue") 監聽名爲consumer.queue的隊列
 */

@Component
@RabbitListener(queues = "consumer.queue")
public class RabbitDealConsumer {

    @Autowired
    private AmqpTemplate rabbitmqTemplate;

    private Logger logger = LoggerFactory.getLogger(RabbitDealConsumer.class);

    /**
     * 消費消息
     * @RabbitHandler 代表此方法爲接受到消息後的處理方法
     */
    @RabbitHandler
    public void receiveMessage(Map msg){
        logger.info("經過死信之後,消費者接收到的消息 :" + msg.toString());
    }

}

四.測試

首先啓動生產者rabbitmq-produce項目。在postman或瀏覽器上訪問:
http://localhost:8783/TestController/dealTest POST請求
這時可以在rabbitmq-produce的控制檯可以看到
在這裏插入圖片描述
然後再啓動消費者rabbitmq-consumer工程,在rabbitmq-consumer可以看到:
在這裏插入圖片描述

下一章,學習 消費端限流

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