消息队列之RabbitMQ死信队列详解

一,死信队列是什么

死信,在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。那么死信是个什么东西呢?

“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况
1,消息被否定确认,使用 channel.basicNack 或channel.basicReject ,并且此时requeue 属性被设置为false。
2,消息在队列的存活时间超过设置的TTL时间。
3,消息队列的消息数量已经超过最大队列长度。

那么该消息将成为“死信”。

“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。

二,如何配置死信队列

大概可以分为以下步骤:

1,配置业务队列,绑定到业务交换机上
2,为业务队列配置死信交换机和路由key
3,为死信交换机配置死信队列

注意,并不是直接声明一个公共的死信队列,然后所以死信消息就自己跑到死信队列里去了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key。

有了死信交换机和路由key后,接下来,就像配置业务队列一样,配置死信队列,然后绑定在死信交换机上。也就是说,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列。

这里省略了RabbitMQ环境的部署和搭建环节,以直连交换机为例。

(一)提供者

1,pom引入GA坐标

<?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.4.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mq</groupId>
    <artifactId>rabbitmq-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rabbitmq-consumer</name>
    <description>Demo project for Spring Boot</description>

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--        糊涂工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2,修改yml文件


server:
  port: 8001
spring:
  application:
    name: rabbitma-provider
    #rabbitmq配置
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: LCHost #虚拟host 可以不设置,使用server默认host
    publisher-confirm-type: correlated #确认消息已发送到交换机 必须配置这个才会确认回调
    publisher-returns: true #确认消息已发送到队列
    listener:
      simple:
#        retry:
#          enabled: true #开启重试
#          max-attempts: 6 #最大重试间隔
#          initial-interval: 3000 #重试间隔
        default-requeue-rejected: false
        acknowledge-mode: manual





这里记得将default-requeue-rejected属性设置为false。

3,添加两个config类一个死信队列,一个业务队列

死信交换机和队列

package com.mq.rabbitmqprovider.config;

import com.rabbitmq.client.AMQP;
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;

/**
 * 死信队列
 * @author LiuCheng
 * @date 2020/12/30 14:13
 */
@Configuration
public class DeadRabbitMQConfig {
    
    
    /**
     * 死信交换机
     */
    public static final String DEAD_LETTER_EXCHANGE = "deadLetterExchange";
    /**
     * 死信队列路由KEY
     */
    public static final String DEAD_LETTER_ROUTING_KEYA = "deadLetterRoutingkeyA";
    /**
     * 死信队列名称
     */
    public static final String DEAD_LETTER_QUEUEA = "deadLetterQueueA";
    @Bean
    public DirectExchange deadExchange(){
    
    
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }
    @Bean
    public Queue deadQueue(){
    
    
        return new Queue(DEAD_LETTER_QUEUEA);
    }
    @Bean
    public Binding bindingDeadLetterExchangeA(){
    
    
        return BindingBuilder.bind(deadQueue()).to(deadExchange()).with(DEAD_LETTER_ROUTING_KEYA);
    }
}

业务交换机和队列

package com.mq.rabbitmqprovider.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 LiuCheng
 * @data 2020/12/30 14:31
 */
@Configuration
public class BusinessRabbitMQConfig {
    
    
    public static final String BUSINESS_EXCHANGE = "businessExchange";
    public static final String BUSINESS_QUEUE = "businessQueue";
    public static final String BUSINESS_ROUTE_KEY = "businessRouteKey";
    @Bean
    public DirectExchange businessExchange(){
    
    
        return new DirectExchange(BUSINESS_EXCHANGE);
    }
    @Bean
    public Queue businessQueue(){
    
    
        Map<String,Object> map =new HashMap<>();
        //这里声明当前队列绑定的死信交换机
        map.put("x-dead-letter-exchange",DeadRabbitMQConfig.DEAD_LETTER_EXCHANGE);
        //这里声明当前队列的死信路由key
        map.put("x-dead-letter-routing-key",DeadRabbitMQConfig.DEAD_LETTER_ROUTING_KEYA);
        return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(map).build();
//        return QueueBuilder.durable().withArguments(map).build();
    }
    @Bean
    public Binding bindBusinessExchange(){
    
    
        return BindingBuilder.bind(businessQueue()).to(businessExchange()).with(BUSINESS_ROUTE_KEY);
    }
}

4,控制层

package com.mq.rabbitmqprovider.controller;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONNull;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.mq.rabbitmqprovider.config.BusinessRabbitMQConfig;
import com.mq.rabbitmqprovider.config.DeadRabbitMQConfig;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * @author LiuCheng
 * @data 2020/12/30 15:03
 */
@RestController
@RequestMapping("/mq/business")
public class BusinessController {
    
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @GetMapping("/send")
    public String send(String msg){
    
    
        String messageId= String.valueOf(UUID.randomUUID());
        String creatTime= DateUtil.now();
        Map<String,Object> map = new HashMap<>();
        map.put("messageId",messageId);
        map.put("messgageName",msg);
        map.put("creatTime",creatTime);
        rabbitTemplate.convertAndSend(BusinessRabbitMQConfig.BUSINESS_EXCHANGE,BusinessRabbitMQConfig.BUSINESS_ROUTE_KEY, JSONUtil.toJsonStr(map));
        return "ok";
    }
}

启动提供者,查看队列列表中的数据发现多了两个队列,一个也对队列businessQueue和一个死信队列deadLetterQueueA

二,消费者

pom文件和yml文件和提供者一样,只是端口有所变化
1,业务队列消费者

package com.mq.rabbitmqconsumer.rabbimqConsumer;

import cn.hutool.json.JSONUtil;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

/**
 * 业务消费者
 * @author LiuCheng
 * @data 2020/12/30 15:13
 */
@Component

public class BusinessReceiver {
    
    
    @RabbitListener(queues = "businessQueue")
    public void process(Message message, Channel channel) throws IOException {
    
    

        try {
    
    
//            int i=10/0;
            System.out.println("业务消费者接受到的信息是 :  "+ JSONUtil.parse( new String (message.getBody(),"UTF-8")));
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
    
    
            System.out.println("业务消费者发生异常");
            channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
            e.printStackTrace();
        }
    }
}

启动消费者,请求提供者业务类的接口,消费正常
在这里插入图片描述
在这里插入图片描述
此时放开异常 int i=10/0,再次发送请求,控制台报错,在死信队列中会有一条消息
在这里插入图片描述
在这里插入图片描述

2,死信队列消费者

package com.mq.rabbitmqconsumer.rabbimqConsumer;

import cn.hutool.json.JSONUtil;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

/**
 * 死信队列消费者
 *
 * @author LiuCheng
 * @data 2020/12/30 15:47
 */
@Component
public class DeadReceiver {
    
    
    @RabbitListener(queues = "deadLetterQueueA")
    public void process(Message message, Channel channel) throws IOException {
    
    
        System.out.println("死信队列消费者 :  "+ JSONUtil.parse( new String (message.getBody(),"UTF-8")));
        // 必须手动确认否则消息一直存在队列中,每次启动会被消费一次
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);

    }
}

启动后发现在死信队列中的消息被消费掉
在这里插入图片描述
再看下向提供者发送请求,消费者异常,由死信队列消费流程

业务消费者发生异常
java.lang.ArithmeticException: / by zero
	at com.mq.rabbitmqconsumer.rabbimqConsumer.BusinessReceiver.process(BusinessReceiver.java:25)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:171)
	at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:120)
	at org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter.invoke(HandlerAdapter.java:53)
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:221)
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandlerAndProcessResult(MessagingMessageListenerAdapter.java:149)
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:134)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1632)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.actualInvokeListener(AbstractMessageListenerContainer.java:1551)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer$$Lambda$499/11521473.invokeListener(Unknown Source)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.retry.interceptor.RetryOperationsInterceptor$1.doWithRetry(RetryOperationsInterceptor.java:93)
	at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:329)
	at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:225)
	at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:116)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at org.springframework.amqp.rabbit.listener.$Proxy68.invokeListener(Unknown Source)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:1539)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:1530)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:1474)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:966)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:912)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1600(SimpleMessageListenerContainer.java:83)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.mainLoop(SimpleMessageListenerContainer.java:1287)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1193)
	at java.lang.Thread.run(Thread.java:745)
死信队列消费者 :  {
    
    "messageId":"382eccb7-0870-4ac5-910f-21b9ca4498de","creatTime":"2020-12-30 17:00:17","messgageName":"hello"}

可以看到,死信队列的Consumer接受到了这个消息,所以流程到此为止就打通了

三,死信消息的变化

那么“死信”被丢到死信队列中后,会发生什么变化呢?
如果队列配置了参数 x-dead-letter-routing-key 的话,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key。
举个栗子:
如果原有消息的路由key是testA,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key,则该消息成为死信后,将保留原有的路由keytestA,如果配置了该参数,并且值设置为testB,那么该消息成为死信后,路由key将会被替换为testB,然后被抛到死信交换机中。
另外,由于被抛到了死信交换机,所以消息的Exchange Name也会被替换为死信交换机的名称。
消息的Header中,也会添加很多奇奇怪怪的字段,修改一下上面的代码,在死信队列的消费者中添加一行日志输出:

log.info("死信消息properties:{}", message.getMessageProperties());

然后重新运行一次,即可得到死信消息Header中被添加的信息

在这里插入代码片死信消息properties:MessageProperties [headers={
    
    x-first-death-exchange=dead.letter.demo.simple.business.exchange, x-death=[{
    
    reason=rejected, count=1, exchange=dead.letter.demo.simple.business.exchange, time=Sun Jul 14 16:48:16 CST 2019, routing-keys=[], queue=dead.letter.demo.simple.business.queuea}], x-first-death-reason=rejected, x-first-death-queue=dead.letter.demo.simple.business.queuea}, correlationId=1, replyTo=amq.rabbitmq.reply-to.g2dkABZyYWJiaXRAREVTS1RPUC1DUlZGUzBOAAAPQAAAAAAB.bLbsdR1DnuRSwiKKmtdOGw==, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=dead.letter.demo.simple.deadletter.exchange, receivedRoutingKey=dead.letter.demo.simple.deadletter.queuea.routingkey, deliveryTag=1, consumerTag=amq.ctag-NSp18SUPoCNvQcoYoS2lPg, consumerQueue=dead.letter.demo.simple.deadletter.queuea]

Header中看起来有很多信息,实际上并不多,只是值比较长而已。下面就简单说明一下Header中的值:
x-first-death-exchange:第一次被抛入的死信交换机的名称

x-first-death-reason: 第一次成为死信的原因,rejected:消息在重新进入队列时被队列拒绝,由于default-requeue-rejected 参数被设置为false。expired :消息过期。maxlen : 队列内消息数量超过队列最大容量

x-first-death-queue:第一次成为死信前所在队列名称

x-death:历次被投入死信交换机的信息列表,同一个消息每次进入一个死信交换机,这个数组的信息就会被更新

四,死信队列的应用场景

通过上面的信息,我们已经知道如何使用死信队列了,那么死信队列一般在什么场景下使用呢?

一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息(没错,以前就是这么干的= =)。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。

五,总结

死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。

总结一下死信消息的生命周期:

1,业务消息被投入业务队列
2,消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了nck或者reject操作
3,被nck或reject的消息由RabbitMQ投递到死信交换机中
4,死信交换机将消息投入相应的死信队列
5,死信队列的消费者消费死信消息

死信消息是RabbitMQ为我们做的一层保证,其实我们也可以不使用死信队列,而是在消息消费异常时,将消息主动投递到另一个交换机中,当你明白了这些之后,这些Exchange和Queue想怎样配合就能怎么配合。比如从死信队列拉取消息,然后发送邮件、短信、钉钉通知来通知开发人员关注。或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费。

猜你喜欢

转载自blog.csdn.net/HBliucheng/article/details/111993550