RabbitMQ笔记(四)SpringBoot整合RabbitMQ之死信队列实践

目录

    • 一、背景
    • 二、Maven依赖
    • 三、核心配置类
      • 3.1 RabbitMQ配置类
      • 3.2 自定义属性配置类
      • 3.3 公共包里的类
      • 3.4 交换机、队列、路由、死信队列
    • 四、业务处理类
    • 五、配置文件
    • 六、测试类TestDelayQueueService
    • 结语

一、背景

  本文主要用使用Spring Boot(2.5.2)来整合RabbitMQ(2.5.2),使用的是simple容器实现的消费者。本文的前提是有一个安装好的RabbitMQ的环境,及我的上一篇文章里生产者服务:
  链接: RabbitMQ笔记(一)SpringBoot整合RabbitMQ之simple容器(消费者)
  链接: RabbitMQ笔记(二)SpringBoot整合RabbitMQ之simple容器(生产者)
  链接: RabbitMQ笔记(三)RabbitMQ持久化的几个姿势(Spring Boot版本)

二、Maven依赖

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.2version>
        <relativePath/> 
    parent>
    <groupId>com.aliangroupId>
    <artifactId>dlqartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>dlqname>
    <description>SpringBoot整合RabbitMQ之死信队列description>

    <properties>
        <java.version>1.8java.version>
    properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
            <version>${parent.version}version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <version>${parent.version}version>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
            <version>${parent.version}version>
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.68version>
        dependency>

        
        <dependency>
            <groupId>com.fasterxml.jackson.coregroupId>
            <artifactId>jackson-databindartifactId>
            <version>2.9.10version>
        dependency>

        
        <dependency>
            <groupId>com.fasterxml.jackson.datatypegroupId>
            <artifactId>jackson-datatype-jsr310artifactId>
            <version>2.9.10version>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.16version>
        dependency>

        
        <dependency>
            <groupId>com.aliangroupId>
            <artifactId>commonartifactId>
            <version>0.0.1-SNAPSHOTversion>
        dependency>

    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

  这里需要注意的是下面这个包,是我本人打包到私服的,本文中主要用到一个查询包装类,加上一个常量类,在我上几篇文章里也提过,就不多说了。

<dependency>
    <groupId>com.aliangroupId>
    <artifactId>commonartifactId>
    <version>0.0.1-SNAPSHOTversion>
dependency>

  这里也给个打包的命令:

call mvn clean source:jar deploy -Dmaven.test.skip=true

三、核心配置类

  这个配置类和我之前文章RabbitMQ笔记(一)SpringBoot整合RabbitMQ之simple容器(消费者)讲的是一样的,直接拷贝过来的,只不过我为了方便测试,把消息应答模式改为了自动应答(AcknowledgeMode.AUTO

3.1 RabbitMQ配置类

SimpleRabbitMqConfig.java

package com.alian.dlq.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

@Configuration
public class SimpleRabbitMqConfig {
     

    /**
     * SimpleMessageListenerContainer
     *
     * @param connectionFactory
     * @return
     */
    @Bean(name = "simpleContainerFactory")
    public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
     
        SimpleRabbitListenerContainerFactory simpleContainerFactory = new SimpleRabbitListenerContainerFactory();
        //设置连接工厂
        simpleContainerFactory.setConnectionFactory(connectionFactory);
        //接收消息采用Jackson2JsonMessageConverter序列化
        simpleContainerFactory.setMessageConverter(this.jackson2JsonMessageConverter());
        //设置初始消费者数量(SimpleRabbitListenerContainerFactory配置类的配置优先级比配置文件高)
        simpleContainerFactory.setConcurrentConsumers(2);
        //设置最大消费者数量(SimpleRabbitListenerContainerFactory配置类的配置优先级比配置文件高)
        simpleContainerFactory.setMaxConcurrentConsumers(10);
        //设置消费者每次获取的消息数,默认250(SimpleRabbitListenerContainerFactory配置类的配置优先级比配置文件高)
        simpleContainerFactory.setPrefetchCount(30);
        //应答模式NONE:不确认模式,MANUAL:手动确认模式,AUTO:自动确认模式
        simpleContainerFactory.setAcknowledgeMode(AcknowledgeMode.AUTO);
        //消费者listener抛出异常,是否重回队列,默认true:重回队列, false为不重回队列(结合死信交换机)
        simpleContainerFactory.setDefaultRequeueRejected(false);
        return simpleContainerFactory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
     
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        //设置连接工厂
        rabbitTemplate.setConnectionFactory(connectionFactory);
        //接收消息采用Jackson2JsonMessageConverter序列化(支持java 8时间)
        rabbitTemplate.setMessageConverter(this.jackson2JsonMessageConverter());
        //Mandatory为true时,消息通过交换器无法匹配到队列会返回给生产者,为false时匹配不到会直接被丢弃
        rabbitTemplate.setMandatory(true);
        return rabbitTemplate;
    }

    @Bean("jacksonMessageConverter")
    public MessageConverter jackson2JsonMessageConverter() {
     
        ObjectMapper mapper = getMapper();
        return new Jackson2JsonMessageConverter(mapper);
    }

    /**
     * 使用com.fasterxml.jackson.databind.ObjectMapper
     * 对数据进行处理包括java8里的时间
     *
     * @return
     */
    private ObjectMapper getMapper() {
     
        ObjectMapper mapper = new ObjectMapper();
        //设置可见性
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //默认键入对象
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //设置Java 8 时间序列化
        JavaTimeModule timeModule = new JavaTimeModule();
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        timeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        //禁用把时间转为时间戳
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        //遇到未知属性或者属性不匹配的时候不抛出异常
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.registerModule(timeModule);
        return mapper;
    }

}

3.2 自定义属性配置类

AppProperties.java

package com.alian.dlq.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@Component
@ConfigurationProperties(value = "app")
public class AppProperties {
     

    /**
     * 查询间隔
     */
    private List<Integer> queryGrap;

    /**
     * 最大查询次数
     */
    private int maxQueryCount;

}

3.3 公共包里的类

  MQConstants放到公共包里的原因就是多个系统都可以共用,比如我之前文章用到的也在这个里面,QueryDto放到公共包是因为消息反序列化问题,反序列化是要同一个包路径,否则就会发生异常(比如:在生产者里发送消息序列化的路径是“com.alian.common.dto.QueryDto”,而你在消费这服务里自己拷贝了一份QueryDto,然后包路径变成“com.alian.rabbitmq.dto.QueryDto”,这样就会出现异常),我之前的文章有详细的介绍。

MQConstants.java

package com.alian.common.constant;

public class MQConstants {
     

    /**
     * 交换机
     */
    public final static String ALIAN_EXCHANGE_NAME = "ALIAN_EXCHANGE";
    public final static String PT_EXCHANGE_NAME = "PT_EXCHANGE";
    //死信交换机
    public final static String PT_DELAY_EXCHANGE_NAME = "PT_DELAY_EXCHANGE";

    /**
     * 队列名
     */
    public final static String ALIAN_QUEUE_NAME = "ALIAN_QUEUE";
    public final static String OIS_QUEUE_NAME = "OIS_QUEUE";
    //死信队列
    public final static String OIS_DELAY_QUEUE_LEVEL1_NAME = "OIS_DELAY_QUEUE_LEVEL1";
    public final static String OIS_DELAY_QUEUE_LEVEL2_NAME = "OIS_DELAY_QUEUE_LEVEL2";
    public final static String OIS_DELAY_QUEUE_LEVEL3_NAME = "OIS_DELAY_QUEUE_LEVEL3";
    public final static String OIS_DELAY_QUEUE_LEVEL4_NAME = "OIS_DELAY_QUEUE_LEVEL4";
    public final static String OIS_DELAY_QUEUE_LEVEL5_NAME = "OIS_DELAY_QUEUE_LEVEL5";

    /**
     * 路由key
     */
    public final static String ALIAN_ROUTINGKEY_NAME = "ALIAN_ROUTINGKEY";
    public final static String OIS_ROUTINGKEY_NAME = "OIS_ROUTINGKEY";
    //死信队列路由
    public final static String OIS_DELAY_ROUTINGKEY_LEVEL1_NAME = "OIS_DELAY_ROUTINGKEY_LEVEL1";
    public final static String OIS_DELAY_ROUTINGKEY_LEVEL2_NAME = "OIS_DELAY_ROUTINGKEY_LEVEL2";
    public final static String OIS_DELAY_ROUTINGKEY_LEVEL3_NAME = "OIS_DELAY_ROUTINGKEY_LEVEL3";
    public final static String OIS_DELAY_ROUTINGKEY_LEVEL4_NAME = "OIS_DELAY_ROUTINGKEY_LEVEL4";
    public final static String OIS_DELAY_ROUTINGKEY_LEVEL5_NAME = "OIS_DELAY_ROUTINGKEY_LEVEL5";
}

QueryDto.java

package com.alian.common.dto;

import lombok.Data;
import java.io.Serializable;
import java.util.Objects;

@Data
public class QueryDto implements Serializable {
     

    private static final long serialVersionUID = 1L;

    /**
     * 交易流水
     */
    private String tranSeq = "";

    /**
     * 第几次查询
     */
    private int queryCount = 0;

}

3.4 交换机、队列、路由、死信队列

  我这里只是定义了三个死信队列进行演示,你们可以根据自己的业务需要定义多个,注意不要绑定错误即可。

DeadLetterConfig.java

package com.alian.dlq.config;

import com.alian.common.constant.MQConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class DeadLetterConfig {
     

    public static Map<String, Object> queueParams = new HashMap<>();

    /**
     * 消息过期后,都发送到OIS_QUEUE
     */
    static {
     
        //消息在队列中存活的时间
        queueParams.put("x-message-ttl", 8 * 60 * 60 * 1000);
        //消息过期后要发送的交换机
        queueParams.put("x-dead-letter-exchange", MQConstants.PT_EXCHANGE_NAME);
        //消息过期后要发送的路由
        queueParams.put("x-dead-letter-routing-key", MQConstants.OIS_ROUTINGKEY_NAME);
    }

    /**
     * 定义交换机(持久化)
     * 

* name:交换机的名称 * durable:设置是否持久化。持久化可以将交换机存盘,在服务器重启的时候不会丢失相关信息 * autoDelete:在所在消费者都解除订阅的情况下自动删除 */ @Bean public DirectExchange defaultExchange() { return new DirectExchange(MQConstants.PT_EXCHANGE_NAME, true, false); } /** * 定义一个队列(持久化) *

* name:队列的名称 * durable:设置是否持久化。持久化的队列会存盘,在RabbitMQ服务重启的时候可以保证不丢失相关信息 * * @return */ @Bean public Queue oisQueue() { return new Queue(MQConstants.OIS_QUEUE_NAME, true); } /** * 绑定队列,通过指定交换机和路由key把消息发送到指定的队列(一个队列可以绑定多个路由key) * * @return */ @Bean public Binding oisQueueBinding() { return BindingBuilder.bind(oisQueue()).to(defaultExchange()).with(MQConstants.OIS_ROUTINGKEY_NAME); } /** * 死信队列交换机 */ @Bean public DirectExchange delayExchange() { return new DirectExchange(MQConstants.PT_DELAY_EXCHANGE_NAME, true, false); } /** * 声明死信队列 */ @Bean() public Queue oisDelayQueueLevel1() { return new Queue(MQConstants.OIS_DELAY_QUEUE_LEVEL1_NAME, true, false, false, queueParams); } @Bean() public Queue oisDelayQueueLevel2() { return new Queue(MQConstants.OIS_DELAY_QUEUE_LEVEL2_NAME, true, false, false, queueParams); } @Bean() public Queue oisDelayQueueLevel3() { return new Queue(MQConstants.OIS_DELAY_QUEUE_LEVEL3_NAME, true, false, false, queueParams); } /** * 绑定死信队列(我这里用的交换机和普通队列区别开了) */ @Bean public Binding oisDelayQueueLevel1binding() { return BindingBuilder.bind(oisDelayQueueLevel1()).to(delayExchange()).with(MQConstants.OIS_DELAY_ROUTINGKEY_LEVEL1_NAME); } @Bean public Binding oisDelayQueueLevel2binding() { return BindingBuilder.bind(oisDelayQueueLevel2()).to(delayExchange()).with(MQConstants.OIS_DELAY_ROUTINGKEY_LEVEL2_NAME); } @Bean public Binding oisDelayQueueLevel3binding() { return BindingBuilder.bind(oisDelayQueueLevel3()).to(delayExchange()).with(MQConstants.OIS_DELAY_ROUTINGKEY_LEVEL3_NAME); } }

上述代码绑定的结果如表格:

队列类型 交换机 路由 队列
普通队列 PT_EXCHANGE OIS_ROUTINGKEY OIS_QUEUE
死信队列 PT_DELAY_EXCHANGE OIS_DELAY_ROUTINGKEY_LEVEL1 OIS_DELAY_QUEUE_LEVEL1
死信队列 PT_DELAY_EXCHANGE OIS_DELAY_ROUTINGKEY_LEVEL2 OIS_DELAY_QUEUE_LEVEL2
死信队列 PT_DELAY_EXCHANGE OIS_DELAY_ROUTINGKEY_LEVEL3 OIS_DELAY_QUEUE_LEVEL3

  下面这个作一个简单的解释,声明的那三个死信队列的消息过期后,会通过指定的交换机和路由发送出去,最终是到达队列OIS_QUEUE,我这里设置他们在队列中最大过期时间为8小时(根据自己需要设置),但是消息的过期时间是可以由生产者设置的,但是最好不要超过队列消息的过期时间,否则可能会出现消息丢失。

    public static Map<String, Object> queueParams = new HashMap<>();

    /**
     * 消息过期后,都发送到OIS_QUEUE
     */
    static {
     
        //消息在队列中存活的时间
        queueParams.put("x-message-ttl", 8 * 60 * 60 * 1000);
        //消息过期后要发送的交换机
        queueParams.put("x-dead-letter-exchange", MQConstants.PT_EXCHANGE_NAME);
        //消息过期后要发送的路由
        queueParams.put("x-dead-letter-routing-key", MQConstants.OIS_ROUTINGKEY_NAME);
    }

四、业务处理类

业务逻辑的说明:

  • 消费者接收到消息后,先获取已查询的次数和以及配置的最大查询次数,如果达到最大查询次数则不再查询
  • 执行查询任务
  • 查询次数加1
  • 查询得到想要的结果,结束查询
  • 如果当前查询次数(加1次后)达到最大的查询次数,则结束查询
  • 发送mq消息到私信队列(最后再次回到本流程的第一步)

QueryPayResultService.java

package com.alian.dlq.service;

import com.alian.common.constant.MQConstants;
import com.alian.common.dto.QueryDto;
import com.alian.dlq.config.AppProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;

/**
 * 注意这里监听的是OIS_QUEUE,不是监听死信队列
 */
@Slf4j
@Service
@RabbitListener(queues = MQConstants.OIS_QUEUE_NAME, containerFactory = "simpleContainerFactory")
public class QueryPayResultService {
     

    @Autowired
    private AppProperties appProperties;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RabbitHandler
    public void processEmployee(QueryDto queryDto) throws Exception {
     
        log.info("----------开始处理queryDto----------");
        log.info("接收到的queryDto信息: {}", queryDto);
        //获取当前消息已查询次数(生产者首次发过来的时候是0)
        int queryCount = queryDto.getQueryCount();
        //获取配置的最大的查询次数
        int maxQueryCount = appProperties.getMaxQueryCount();
        //先判断查询次数是否超过最大值
        if (queryCount >= maxQueryCount) {
     
            log.info("已达到最大查询次数,不再查询");
            return;
        }
        //执行查询的任务,我这里就模拟得了
        boolean b = queryPayResult();
        //查询次数加1
        queryCount = queryCount + 1;
        log.info("第【{}】次查询结果返回:{}, ", queryCount, b);
        if (b) {
     
            log.info("查询成功,不再查询");
            //做业务处理
            //...
            return;
        }
        if (queryCount == 3) {
     
            log.info("达到最大查询次数,不再查询");
            //做业务处理
            //...
            return;
        }
        queryDto.setQueryCount(queryCount);
        //做业务处理
        //...
        //发送消息到下一个死信队列
        sendMsgToDelayQueue(queryDto);
        log.info("----------queryDto处理完成----------");
        //如果是手动应答模式:AcknowledgeMode.MANUAL 则需要调用
    }

    private void sendMsgToDelayQueue(QueryDto queryDto) {
     
        int queryCount = queryDto.getQueryCount();
        MessagePostProcessor processor = message -> {
     
            List<Integer> queryGrap = appProperties.getQueryGrap();
            Integer queryGrapTime = queryGrap.get(queryCount);
            log.info("第【{}】次查询结果返失败, {}秒后再查询:", queryCount, queryGrapTime);
            //消息持久化
            message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
            //消息过期时间,单位毫秒
            message.getMessageProperties().setExpiration("" + queryGrapTime * 1000);
            return message;
        };
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend(MQConstants.PT_DELAY_EXCHANGE_NAME, getOisDelayRoutingKey(queryCount + 1), queryDto, processor, correlationData);
    }

    /**
     * 获取当前查询次数后,应该使用的路由key
     *
     * @param queryCount
     * @return
     */
    private String getOisDelayRoutingKey(int queryCount) {
     
        return "OIS_DELAY_ROUTINGKEY_LEVEL" + queryCount;
    }

    /**
     * 模拟查询结果
     *
     * @return
     */
    private boolean queryPayResult() {
     
        //我这里模拟查询,生成1-100内的随机数,如果小于20则任务查询成功。
        int roundNum = (int) Math.round(Math.random() * (100 - 1) + 1);
        return roundNum < 20;
    }

}

五、配置文件

application.yml

#项目名和端口
server:
  port: 8080
  servlet:
    context-path: /rabbitmq-dlq

#RabbitMQ配置
spring:
  rabbitmq:
    #地址
    addresses: 192.168.0.194
    #端口
    port: 5672
    #用户名
    username: test
    #密码
    password: test
    #连接到代理时用的虚拟主机
    virtual-host: /
    #消费者相关配置
    listener:
      type: simple

app:
  #最大查询次数
  max-query-count: 3
  #查询间隔
  query-grap:
    - 5
    - 10
    - 15

六、测试类TestDelayQueueService

  之前我说过,测试生产者和消费者时,最好使用两个系统测试,不然很多问题你会觉得很奇怪,比如序列化问题。
现在我在我之前的文章里加个测试类。链接: RabbitMQ笔记(二)SpringBoot整合RabbitMQ之simple容器(生产者)

TestDelayQueueService.java

package com.alian.publish.service;

import com.alian.common.constant.MQConstants;
import com.alian.common.dto.QueryDto;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.UUID;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestDelayQueueService {
     

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendMsgToDelayQueue() {
     
        QueryDto queryDto = new QueryDto();
        //交易流水
        queryDto.setTranSeq("20210901" + System.currentTimeMillis());
        //查询次数
        queryDto.setQueryCount(0);
        MessagePostProcessor processor = message -> {
     
            message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
            //过期时间
            message.getMessageProperties().setExpiration("" + 5 * 1000);
            return message;
        };
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //发送消息到第一个死信队列,注意路由不要写错了
        rabbitTemplate.convertAndSend(MQConstants.PT_DELAY_EXCHANGE_NAME, MQConstants.OIS_DELAY_ROUTINGKEY_LEVEL1_NAME, queryDto, processor, correlationData);
        try {
     
            //防止生产者发送消息后,关闭了服务,消息回调异常(通道关闭)
            Thread.sleep(5000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
    }

}

最后我得再次提醒下,很多小伙伴可能按照我这个执行的时候会出现如下的错误:

消息发送失败,原因为:clean channel shutdown; protocol method: #method<channel.close>(reply-code=200, reply-text=OK, class-id=0, method-id=0)

这是因为ConfirmCallback是异步的,我们使用junit测试发送完消息后就关闭了,也就断开了连接,所以测试时候可以加入一个休眠代码,如上例,或者采用@PostConstruct进行测试。

    @PostConstruct
    public void sendMsgToDelayQueue() {
     

    }

发送消息到死信队列完成查询:

2021-09-01 14:32:32 064 INFO :----------开始处理queryDto----------
2021-09-01 14:32:32 064 INFO :接收到的queryDto信息: QueryDto{tranSeq='202109011630477946906', queryCount=0}
2021-09-01 14:32:32 065 INFO :第【1】次查询结果返回:false, 
2021-09-01 14:32:32 078 INFO :间隔:[5, 10, 15]
2021-09-01 14:32:32 078 INFO :第【1】次查询结果返失败, 10秒后再查询:
2021-09-01 14:32:32 084 INFO :----------queryDto处理完成----------
2021-09-01 14:32:42 094 INFO :----------开始处理queryDto----------
2021-09-01 14:32:42 095 INFO :接收到的queryDto信息: QueryDto{tranSeq='202109011630477946906', queryCount=1}
2021-09-01 14:32:42 095 INFO :第【2】次查询结果返回:false, 
2021-09-01 14:32:42 095 INFO :间隔:[5, 10, 15]
2021-09-01 14:32:42 095 INFO :第【2】次查询结果返失败, 15秒后再查询:
2021-09-01 14:32:42 095 INFO :----------queryDto处理完成----------
2021-09-01 14:32:57 102 INFO :----------开始处理queryDto----------
2021-09-01 14:32:57 102 INFO :接收到的queryDto信息: QueryDto{tranSeq='202109011630477946906', queryCount=2}
2021-09-01 14:32:57 102 INFO :第【3】次查询结果返回:true, 
2021-09-01 14:32:57 102 INFO :查询成功,不再查询

从结果上我们可以看到,我们总共完成3次查询才查询到结果(随机的),并且是按照我们设定的间隔时间段进行查询的,也没有超过我们设定的最大查询次数,实际中你可以设置多个队列,然后设置不同的时间梯度,完成你的功能,比如:需要异步消息处理的梯度为:15s/15s/30s/3m/10m/20m/30m/30m/60m,你设置9个队列,对应上时间间隔,然后结合我的实例就行了。

重要知识点提醒:

  • 很多人觉得搞一个队列就行了,所有消息发送到同一个队列,这种方式成立的一个可能就是,所有的时间间隔都是一样的,比如都是30秒,那么你使用一个队列也是能够实现这个功能的。
  • 但是如果你的时间间隔(队列消息过期时间)不一样,那么你可能就会得到不一样的结果。比如:你只有一个队列,想实现20秒处理一次业务,30秒后再处理一次业务,假设你向死信队列发送4条消息(标记为A、B、C、D),消息过期时间分别是20s,30s,20s,20s,实际上你20秒的时候,消费者只会收到1条消息(A),不是3条(ACD),到30秒的时候,你会一次性收到3条消息(BCD),是不是很意外?。后面的消息根本没有按你预想的20秒后执行,而是30秒后才执行。
  • 为什么呢?这个是队列,也就是按顺序来的排队的,当到第一条消息消费后,第二条消息B到达队首,要到30秒才过期,实际后面的两条消息CD过期了(20s),也不会被转移走,因为前面还有消息,只有到30秒时,第二条消息B到期了,才会三条消息BCD一起被消费者消费。
  • 如果你发送的消息过期时间更长,比如1小时,那么它之后的消息都会等1小时,直到此消息从队列移走,这就可能出现很大的生产事故了,比如消息处理不及时,或者队列消息积压。所以你就能理解时间梯队不一样,为什么要设置多个不同的队列了,并且发送消息的时间尽量保持一致(小于也是可以的),也就能理解为什么时间间隔一样,使用一个队列也行了。

结语

  关于本次RabbitMQ中的死信队列就介绍到这里,用到本例的场景可以用到很多的查询,比如延迟查询或者消息推送,比如支付查询,短信推送,邮件发送,也可以用于消息及时通知(结果异步实时梯度通知),不过对于及时消息通知,就不是把消息直接发送到死信队列,而是直接发送到工作队列,也就是消费者监听的队列,只有通知失败,再发送到死信队列,如果有什么疑问也可以评论交流。

你可能感兴趣的:(RabbitMQ笔记,rabbitmq,spring,boot,消息队列)