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实现延时队列实战

下面是死信队列在创建、绑定、生产消息、消费消息过程的结构流程图:
RabbitMQ SpringBoot 延时队列 ( 存活时间TTL 和 死信队列DLX ) 应用_第1张图片
图中问题的答案为:当入死信队列的消息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 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 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可以看到:
在这里插入图片描述

下一章,学习 消费端限流

你可能感兴趣的:(SpringBoot,Rabbit,MQ)