消息中间件 RabbitMQ 之 延迟队列 详解&实战

文章目录

  • 7. 延迟队列
    • 7.1 延迟队列的概念
    • 7.2 延迟队列使用场景
    • 7.4 整合SpringBoot
      • 7.4.1 创建项目
      • 7.4.2 添加依赖
      • 7.4.3 修改配置文件
      • 7.4.4 添加swagger配置类
    • 7.5 队列TTL
      • 7.5.1 代码架构图
      • 7.5.2 配置文件代码
      • 7.5.3 消息生产者代码
      • 7.5.4 消息消费者代码
    • 7.6 延迟队列优化
      • 7.6.1 代码架构图
      • 7.6.2 配置文件类代码
      • 7.6.3 消息生产者代码
    • 7.7 RabbitMQ 插件实现延迟队列
      • 7.7.1 安装延迟队列插件
      • 7.7.2 代码架构图
      • 7.7.3 配置文件类代码
      • 7.7.4 消息生产者代码
      • 7.7.5 消息消费者代码
    • 7.8 总结

7. 延迟队列

7.1 延迟队列的概念


延迟队列,队列内部是有序的,最重要的特性就体现在它的延迟属性上。

延迟队列中的元素是希望在指定时间到了以后或之前取出和处理。简单来说,延迟队列就是用来存放需要在指定时间被处理的元素的队列。

延迟队列属于死信队列的一种,属于消息TTL过期的情况。

7.2 延迟队列使用场景


  1. 订单在十分钟内未支付自动取消
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒
  3. 用户注册成功后,如果三天内没有登陆则进行短信提醒
  4. 用户发起退款,如果三天内没有得到处理则通知相关运维人员
  5. 预定会议后,需要在预定的时间点前十分钟通知通知各个与会人员参加会议

7.4 整合SpringBoot


7.4.1 创建项目


消息中间件 RabbitMQ 之 延迟队列 详解&实战_第1张图片

7.4.2 添加依赖


依赖:


        
            org.springframework.boot
            spring-boot-starter-amqp
        

        
            org.springframework.boot
            spring-boot-devtools
            runtime
            true
        
        
            org.springframework.boot
            spring-boot-configuration-processor
            true
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.amqp
            spring-rabbit-test
            test
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            com.alibaba
            fastjson
            1.1.41
        
        
            io.springfox
            springfox-swagger2
            2.8.0
        
        
            io.springfox
            springfox-swagger-ui
            2.8.0
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    

7.4.3 修改配置文件


spring.rabbitmq.host=47.98.241.39
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456

7.4.4 添加swagger配置类


package com.example.rabbitmq.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: swagger配置类
 * @date 2022/4/24 0024 13:08
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("boot.spring.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("springboot利用swagger构建rabbitmq api接口文档")
                .description("接口")
                .termsOfServiceUrl("https://gitee.com/projects")
                .version("1.0")
                .build();
    }
}

7.5 队列TTL


7.5.1 代码架构图


创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10s 和 40s,再创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

消息中间件 RabbitMQ 之 延迟队列 详解&实战_第2张图片

由于SpringBoot整合了 RabbitMQ,所以队列、交换机等声名只需要在配置文件中配置即可。所以,以上代码架构只需要写三个类:生产者、消费者以及配置文件

其中:

  • 普通交换机X 分别通过 routingKey XA 和 routingKey XB 与 普通队列QA 和 普通队列QB绑定,死信队列QD 通过 routingKey YD 与 死信交换机Y 和 普通队列QA、QB 绑定
  • 由于消息会存放在普通队列中没有消费者消费,所以普通队列中的消息会一直存放直到消息 TTL 过期,在到达过期时间后自动进入死信队列,再由消费者C消费

7.5.2 配置文件代码


package com.example.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * @author 且听风吟
 * @version 1.0
 * @description: TTL队列 具有过期时间的队列配置 配置文件代码
 *               声名 一个普通交换机、两个普通队列、一个死信交换机、一个死信队列
 * @date 2022/4/24 0024 13:50
 */
@Configuration
public class TtlQueueConfig {

    /**
     * 普通交换机名称
     */
    public static final String EXCHANGE_X = "X";
    /**
     * 死信交换机名称
     */
    public static final String EXCHANGE_DEAD_Y = "Y";
    /**
     * 普通队列名称
     */
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    /**
     * 死信队列名称
     */
    public static final String QUEUE_DEAD_D = "QD";

    /**
     * 声名 普通交换机(EXCHANGE_X)
     * @return 普通交换机
     */
    @Bean
    public DirectExchange EXCHANGEX(){
        return new DirectExchange(EXCHANGE_X);
    }

    /**
     * 声名 死信交换机(EXCHANGE_DEAD_Y)
     * @return 死信交换机
     */
    @Bean
    public DirectExchange EXCHANGEDEADY(){
        return new DirectExchange(EXCHANGE_DEAD_Y);
    }

    /**
     * 声名普通队列 QUEUE_A TTL为10s
     * @return 普通队列 QUEUE_A
     */
    @Bean
    public Queue QUEUE_A(){
        //声名参数
        Map<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",EXCHANGE_DEAD_Y);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置 TTL 10s
        arguments.put("x-message-ttl",10000);

        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }

    /**
     * 声名普通队列 QUEUE_B TTL为40s
     * @return 普通队列 QUEUE_B
     */
    @Bean
    public Queue QUEUE_B(){
        //声名参数
        Map<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",EXCHANGE_DEAD_Y);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        //设置 TTL 40s
        arguments.put("x-message-ttl",400000);

        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
    }

    /**
     * 声名死信队列 QUEUE_DEAD_D
     * @return 死信队列 QUEUE_DEAD_D
     */
    @Bean
    public Queue QUEUE_DEAD_D(){
        return QueueBuilder.durable(QUEUE_DEAD_D).build();
    }

    /**
     * 普通队列QA 通过 routingKey XA 绑定 普通交换机X
     * @param QUEUE_A 普通队列QA
     * @param EXCHANGEX 普通交换机X
     * @return 绑定
     */
    @Bean
    public Binding queueABindingX(Queue QUEUE_A,DirectExchange EXCHANGEX){
        return BindingBuilder.bind(QUEUE_A).to(EXCHANGEX).with("XA");
    }

    /**
     * 普通队列QB 通过 routingKey XB 绑定 普通交换机X
     * @param QUEUE_B 普通队列QB
     * @param EXCHANGEX 普通交换机X
     * @return 绑定
     */
    @Bean
    public Binding queueBBindingX(Queue QUEUE_B,DirectExchange EXCHANGEX){
        return BindingBuilder.bind(QUEUE_B).to(EXCHANGEX).with("XB");
    }

    /**
     * 死信队列QD 通过 routingKey YD 绑定 死信交换机Y
     * @param QUEUE_DEAD_D 死信队列QD
     * @param EXCHANGEDEADY 死信交换机Y
     * @return 绑定
     */
    @Bean
    public Binding queueDBindingX(Queue QUEUE_DEAD_D,DirectExchange EXCHANGEDEADY){
        return BindingBuilder.bind(QUEUE_DEAD_D).to(EXCHANGEDEADY).with("YD");
    }
}

7.5.3 消息生产者代码


package com.example.rabbitmq.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 发送延迟消息
 * @date 2022/4/24 0024 17:58
 */
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message){
        log.info("当前时间:{},发送给两个TTL队列:{}",new Date().toString(),message);

        //生产者使用普通交换机X 通过 信道XA 向普通队列QA发送消息
        rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
        //生产者使用普通交换机X 通过 信道XB 向普通队列QB发送消息
        rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);

    }
}

7.5.4 消息消费者代码


package com.example.rabbitmq.consumer;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 队列TTL 的消费
 * @date 2022/4/25 0025 10:18
 */
@Slf4j
@Component
public class DeadLetterQueueConsumer {

    /**
     * 消费者QD接收消息
     */
    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel){
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列的消息:{}",new Date().toString(),msg);
    }

}

启动spring boot项目,如果报空指针异常可以降低spring boot版本或者在配置文件中添加:

spring.mvc.pathmatch.matching-strategy=ant_path_matcher

浏览器发起一个请求:http://localhost:8080/ttl/sendMsg/嘿嘿嘿

观察控制台的消息接收状况:

消息中间件 RabbitMQ 之 延迟队列 详解&实战_第3张图片

  • 第一条消息在十秒后变成死信,再被消费者消费,第二条消息在40s后变成死信,然后被消费者消费

不过,如果这样使用的话,那么每增加一个新的时间需求,就要重新创建一个队列,如果是会议提前通知,岂不是需要创建无数个队列?所以,这里的代码需要优化

7.6 延迟队列优化


7.6.1 代码架构图


  • 在这里新增了一个队列QC,绑定关系如下,该队列不设置TTL时间

消息中间件 RabbitMQ 之 延迟队列 详解&实战_第4张图片

7.6.2 配置文件类代码


在配置文件类中添加如下代码:

/**
     * 普通队列的名称
     */
    public static final String QUEUE_C = "QC";

/**
     * 声名普通队列 QUEUE_C
     * @return
     */
    @Bean
    public Queue QUEUE_C(){
        Map<String, Object> arguments = new HashMap<>();
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",EXCHANGE_DEAD_Y);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key","YD");
        return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
    }

/**
     * 普通队列QC 通过 routingKey XC 绑定 普通交换机X
     * @param QUEUE_C 普通队列QC
     * @param EXCHANGEX 普通交换机X
     * @return 绑定
     */
    @Bean
    public Binding queueCBindingX(Queue QUEUE_C,DirectExchange EXCHANGEX){
        return BindingBuilder.bind(QUEUE_C).to(EXCHANGEX).with("XC");
    }

7.6.3 消息生产者代码


在生产者类中添加如下代码:

@GetMapping("/sendTTLMsg/{message}/{ttlTime}")
    public void sendMsg(String message,String ttlTime){
        log.info("当前时间:{},发送一条时长为{}ms的TTL信息给队列QC:{}",new Date().toString(),ttlTime,message);

        //生产者使用普通交换机X 通过 信道XC 向普通队列QC发送消息
        rabbitTemplate.convertAndSend("X","XC","消息来自队列QC:"+message,msg ->{
            //设置发送消息时的延迟时长
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }

启动项目,发送请求:

http://localhost:8080/ttl/sendTTLMsg/hello1/20000

http://localhost:8080/ttl/sendTTLMsg/hello2/2000

发送了两条消息,第一条消息为hello1,TTL为20s,第二条消息为hello2,TTL为2s。观察到第一条消息先到,而第二条消息后到。

  • 看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置TTL的方式,消息可能并不会按时“死亡”,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期就会丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行

7.7 RabbitMQ 插件实现延迟队列


上面提到的问题,如果不能实现在消息力度上的TTL,并使其在设置的TTL时间即时死亡,就无法设计成一个通用的延迟队列。下面来使用插件来解决这个问题

  • 基于插件实现延迟队列的原理图:

    由交换机实现队列延迟

消息中间件 RabbitMQ 之 延迟队列 详解&实战_第5张图片

7.7.1 安装延迟队列插件


  • 在github上下载:Releases · rabbitmq/rabbitmq-delayed-message-exchange (github.com)

  • 放置到 RabbitMQ 插件目录

    插件目录:/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.28/plugins

  • 在插件目录下执行如下面命令使插件生效:

    rabbitmq-plugins enable rabbitmq_delayed_message_exchange

    消息中间件 RabbitMQ 之 延迟队列 详解&实战_第6张图片

  • 重启RabbitMQ:systemctl restart rabbitmq-server

安装成功之后,在后台管理界面点击新增交换机,选择类型时可以看到新增了 x-delayed-message (延迟消息)类型

消息中间件 RabbitMQ 之 延迟队列 详解&实战_第7张图片

7.7.2 代码架构图


在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:

消息中间件 RabbitMQ 之 延迟队列 详解&实战_第8张图片

7.7.3 配置文件类代码


在我们自定义的交换机中,x-delayed-message (延迟消息类型)是一种新的交换机类型,该类型消息支持延迟投递机制

  • 延迟投递机制:消息传递后并不会立即投递到目标队列中,而是存储在一个 mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。

配置文件类代码:

package com.example.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;

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

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 配置延迟消息类型交换机队列
 * @date 2022/4/26 0026 10:51
 */
@Configuration
public class DelayedQueueConfig {

    /**
     * 队列
     */
    public static final String  DELAYED_QUEUE_NAME = "delayed.queue";
    /**
     * 延迟交换机
     */
    public static final String  DELAYED_EXCHANGE_NAME = "delayed.exchange";
    /**
     * routingKey
     */
    public static final String  DELAYED_ROUTING_KEY = "delayed.routingkey";

    /**
     * 声名队列
     * @return 队列
     */
    @Bean
    public Queue delayedQueue(){
        return new Queue(DELAYED_QUEUE_NAME);
    }

    /**
     * 声名延迟队列交换机 采用direct模式
     * CustomExchange:自定义交换机
     * @return 延迟队列交换机
     */
    @Bean
    public CustomExchange delayedExchange(){
        Map<String, Object> arguments =new HashMap<>();
        arguments.put("x-delayed-type","direct");
        /**
         * 1.交换机的名称
         * 2.交换机的类型
         * 3.是否需要持久化
         * 4.是否需要自动删除
         * 5.其他参数
         */
        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",
                true,false,arguments);
    }

    /**
     * 绑定
     * @param delayedQueue 队列
     * @param delayedExchange 延迟交换机
     * @return 绑定关系
     */
    @Bean
    public Binding delayedBinding(Queue delayedQueue, Exchange delayedExchange){
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }

}

7.7.4 消息生产者代码


配置在SendMsgController中:

//发送基于插件的消息 及 延迟的时间
    @GetMapping("/sendDelayMsg/{message}/{delayTime}")
    public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime){
        log.info("当前时间:{},发送一条时长为{}ms的信息给延迟队列delayed.queue:{}",
                new Date().toString(),delayTime,message);

        //发消息
        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_QUEUE_NAME,
                DelayedQueueConfig.DELAYED_ROUTING_KEY,message, msg ->{
            //设置发送消息时的延迟时长
            msg.getMessageProperties().setDelay(delayTime);
            return msg;
        });
    }

7.7.5 消息消费者代码


package com.example.rabbitmq.consumer;

import com.example.rabbitmq.config.DelayedQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @author 且听风吟
 * @version 1.0
 * @description: 延迟队列消费者
 *               基于插件的延迟消息
 * @date 2022/4/26 0026 13:41
 */
@Slf4j
@Component
public class DelayQueueConsumer {

    //监听消息
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveMsg(Message message){
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到延迟队列的消息:{}",new Date().toString(),msg);
    }

}

发起请求:

http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000

http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRF0Ufqo-1650955216636)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220426141223886.png)]

第二个消息被先消费掉了,符合预期

7.8 总结


  • 延迟队列在需要延时处理的场景下非常有用,使用RabbitMQ来实现延迟队列可以很好的利用RabbitMQ的特性。如:消息可靠发送、消息可靠投递、死信队列来保证消息至少被消费一次,以及未被处理的消息不会被丢弃
  • 通过RabbitMQ集群的特性,可以很好地解决单点故障的问题,不会因为单个节点挂掉导致延迟队列不可用或者消息丢失
  • 延迟队列还有许多其他的选择,比如利用java的 DelayQueue,利用Redis的zset,利用Quartz(定时器)或者利用 kafka 的时间轮。这些方式各有特点,需要看适用的场景

你可能感兴趣的:(中间件,消息队列MQ,java,中间件)