RabbitMQ笔记(四)SpringBoot整合RabbitMQ之死信队列详细使用

一、背景

  本文主要用使用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

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.alian</groupId>
    <artifactId>dlq</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>dlq</name>
    <description>SpringBoot整合RabbitMQ之死信队列</description>

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

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

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

        <!--rabbitMq的版本 版本最好和springboot保持一致-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>${parent.version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.68</version>
        </dependency>

        <!--用于序列化-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.10</version>
        </dependency>

        <!--java 8时间序列化-->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.9.10</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>

        <!--自己打包上传到私服的,用于测试-->
        <dependency>
            <groupId>com.alian</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

    </dependencies>

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

</project>

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

<dependency>
    <groupId>com.alian</groupId>
    <artifactId>common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</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);
    }

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

    /**
     * 定义一个队列(持久化)
     * <p>
     * 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_EXCHANGEOIS_ROUTINGKEYOIS_QUEUE
死信队列PT_DELAY_EXCHANGEOIS_DELAY_ROUTINGKEY_LEVEL1OIS_DELAY_QUEUE_LEVEL1
死信队列PT_DELAY_EXCHANGEOIS_DELAY_ROUTINGKEY_LEVEL2OIS_DELAY_QUEUE_LEVEL2
死信队列PT_DELAY_EXCHANGEOIS_DELAY_ROUTINGKEY_LEVEL3OIS_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中的死信队列就介绍到这里,用到本例的场景可以用到很多的查询,比如延迟查询或者消息推送,比如支付查询,短信推送,邮件发送,也可以用于消息及时通知(结果异步实时梯度通知),不过对于及时消息通知,就不是把消息直接发送到死信队列,而是直接发送到工作队列,也就是消费者监听的队列,只有通知失败,再发送到死信队列,如果有什么疑问也可以评论交流。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Spring Boot中配置死信队列(Dead Letter Queue),您可以按照以下步骤进行操作: 1. 添加RabbitMQ依赖:在您的Spring Boot项目的pom.xml文件中,添加RabbitMQ依赖。例如,使用以下Maven依赖项: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> ``` 2. 配置RabbitMQ连接信息:在application.properties或application.yml文件中,配置RabbitMQ的连接信息,例如: ```properties spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest ``` 3. 创建交换机和队列使用RabbitAdmin或通过注解方式,在您的代码中创建交换机和队列。例如,可以使用@Bean注解创建一个RabbitAdmin bean,并在其上使用@PostConstruct注解来创建交换机和队列。 ```java @Bean public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) { return new RabbitAdmin(connectionFactory); } @PostConstruct public void setupQueues() { rabbitAdmin().declareExchange(new DirectExchange("myExchange")); rabbitAdmin().declareQueue(new Queue("myQueue")); rabbitAdmin().declareBinding(new Binding("myQueue", Binding.DestinationType.QUEUE, "myExchange", "myRoutingKey", null)); } ``` 4. 配置死信队列:创建一个专用的队列来作为死信队列,并将其与原始队列绑定。您可以在队列声明时设置x-dead-letter-exchange和x-dead-letter-routing-key参数来指定死信队列的交换机和路由键。 ```java @PostConstruct public void setupQueues() { rabbitAdmin().declareExchange(new DirectExchange("myExchange")); rabbitAdmin().declareQueue(new Queue("myQueue", false, false, false, new HashMap<String, Object>() {{ put("x-dead-letter-exchange", "dlxExchange"); put("x-dead-letter-routing-key", "dlxRoutingKey");

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值