RabbitMQ本身是具有死信队列死信交换机属性的,延时队列 可以通过死信队列和死信交换机来实现。在电商行业中,通常都会有一个需求:订单超时未支付,自动取消该订单。那么通过RabbitMQ实现的延时队列就是实现该需求的一种方式。

一、死信队列

死信顾名思义,就是死掉的信息,英文是Dead Letter。死信交换机(Dead-Letter-Exchange)和普通交换机没有区别,都是可以接受信息并转发到与之绑定并能路由到的队列,区别在于死信交换机是转发死信的,而和该死信交换机绑定的队列就是死信队列。说的再通俗一点,死信交换机和死信队列其实都只是普通的交换机和队列,只不过接受、转发的信息是死信,其他操作并没有区别。

1.1.死信的条件

称为死信的信息,需要如下几个条件:

  • 消息被消费者拒绝(通过basic.reject 或者 back.nack),并且设置 requeue=false。
  • 消息过期,因为队列设置了TTL(Time To Live)时间。
  • 发送消息时设置消息的生存时间,如果生存时间到了,还没有被消费。
  • 也可以指定某个队列中所有消息的生产时间,如果生存时间到了,还没有被消费
  • 消息被丢弃,因为超过了队列的长度限制。

如下图:

RabbitMQ死信队列和延迟交换机_发送消息

死信队列的应用:

  • 基于死信队列在队列消息已满的情况下,消息也不会丢失
  • 实现延迟消费的效果。比如:下订单时,有15分钟的付款时间

1.2.实现死信队列准备Exchange和Queue

在config包下创建DeadLetterQueueConfig,在里面根据上一章节创建普通的队列、交换机和死信队列、死信交换机,分别进行绑定,然后普通队列需要绑定死信交换机,代码如下:

package com.augus.config;

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

@Configuration
public class DeadLetterQueueConfig {

    // 普通的交换机和队列
    public static final String NORMAL_EXCHANGE = "normal-exchange";
    public static final String NORMAL_QUEUE = "normal-queue";
    public static final String NORMAL_ROUTING_KEY = "normal.#";

    //死信队列和交换机
    public static final String DEAD_EXCHANGE = "dead-exchange";
    public static final String DEAD_QUEUE = "dead-queue";
    public static final String DEAD_ROUTING_KEY = "dead.#";

    /**
     * 创建普通的交换机
     * @return
     */
    @Bean
    public Exchange normalExchange(){
        return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE).build();
    }

    /**
     * 创建普通的队列,然后绑定死信队列和交换机
     * @return
     */
    @Bean
    public Queue normalQueue(){
        //普通队列这里需要绑定死信交换机
        return QueueBuilder
                .durable(NORMAL_QUEUE)
                .deadLetterExchange(DEAD_EXCHANGE) //死信交换机
                .deadLetterRoutingKey("dead.hello") //绑定的routing_key
                .build();
    }

    /**
     * 绑定交换机和队列
     * @param normalQueue 队列
     * @param normalExchange 交换机
     * @return
     */
    @Bean
    public Binding normalBinding(Queue normalQueue,Exchange normalExchange){
        return BindingBuilder.bind(normalQueue).to(normalExchange).with(NORMAL_ROUTING_KEY).noargs();
    }

    /**
     * 创建私信交换机
     * @return
     */
    @Bean
    public Exchange deadExchange(){
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE).build();
    }

    /**
     * 创建普通的队列,然后绑定死信队列和交换机
     * @return
     */
    @Bean
    public Queue deadQueue(){
        return QueueBuilder.durable(DEAD_QUEUE).build();
    }

    /**
     * 绑定交换机和队列
     * @param deadQueue 队列
     * @param deadExchange 交换机
     * @return
     */
    @Bean
    public Binding deadBinding(Queue deadQueue,Exchange deadExchange){
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY).noargs();
    }
}

1.3. 创建消费者

基于消费者进行reject或者nack实现死信效果,在com.augus.deab 包下创建消费者 DeadListener代码如下:

import com.augus.config.DeadLetterQueueConfig;
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;

@Component
public class DeadListener {

    @RabbitListener(queues = DeadLetterQueueConfig.NORMAL_QUEUE)
    public void consume(String msg, Channel channel, Message message) throws IOException {
        System.out.println("接收到normal-queue队列的消息:"+msg);
        //消息被消费者拒绝(通过basic.reject 或者 back.nack),并且设置 requeue=false。
        channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);

        /**
         * channel.basicNack(deliveryTag, multiple, requeue) 是 RabbitMQ 的 Java 客户端中用于拒绝(Nack)一条或多条消息的方法。下面是对该方法的参数进行解释:
         *      deliveryTag:消息的交付标签(delivery tag),用于唯一标识一条消息。通过 message.getMessageProperties().getDeliveryTag() 获取消息的交付标签。
         *      multiple:是否拒绝多条消息。如果设置为 true,则表示拒绝交付标签小于或等于 deliveryTag 的所有消息;如果设置为 false,则只拒绝交付标签等于 deliveryTag 的消息。
         *      requeue:是否重新入队列。如果设置为 true,则被拒绝的消息会重新放回原始队列中等待重新投递;如果设置为 false,则被拒绝的消息会被丢弃。
         */
        channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
    }
}

1.4. 创建生产者

对于之前的三种情况,进行

1.4.1.基于消费者进行reject或者nack实现死信效果

启动springboot项目,让消费者DeadListener,监听队列。然后拒绝消费

创建生成生产者如下,没有什么特殊的只是发送一条消息而已:

package com.augus;

import com.augus.config.DeadLetterQueueConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PublisherDeadTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void publishDead(){
        rabbitTemplate.convertAndSend(DeadLetterQueueConfig.NORMAL_EXCHANGE,"normal.topic","今天晚上有什么安排?");
        System.out.println("消息发送成功");
    }
}

执行发送消息到队列normal-queue,但是由于消费者会执行basic.reject 或者 back.nack拒绝之后,队列中的消息就会交给dead-queue队列中,如下:

RabbitMQ死信队列和延迟交换机_Test_02

1.4.2.消息的生存时间设置

  • 给单独的某条消息设置生存时间

消费者不变,修改生产者代码如下:

package com.augus;

import com.augus.config.DeadLetterQueueConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PublisherDeadTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void publishDead(){
        rabbitTemplate.convertAndSend(DeadLetterQueueConfig.NORMAL_EXCHANGE, "normal.topic", "今天晚上有什么安排?", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //设置消息的生存时间 单位是ms
                message.getMessageProperties().setExpiration("5000");
                return message;
            }
        });
        System.out.println("消息发送成功");
    }
}

删除之前创建的两个队列,做了修改,只有重新创建才能生效,启动项目,让消费者处于监听状态,然后执行生产者发送消息到队列

RabbitMQ死信队列和延迟交换机_spring_03

观察rabbitMQ中

RabbitMQ死信队列和延迟交换机_发送消息_04

等待5s后,消息到了死信队列

RabbitMQ死信队列和延迟交换机_Test_05

  •  给队列设置消息的生存时间

这里是针对整个队列而言的,队列中的消息超过了这个世界还没有被消息,将会放入私信队列,修改config包下的代码,通过ttl设置队列消息的生存时间,如下:

RabbitMQ死信队列和延迟交换机_发送消息_06

删除之前的队列消息,然后执行生产者发送消息到队列,生产者代码如下,生产者就不需要给消息本身在设置生存时间了:

package com.augus;

import com.augus.config.DeadLetterQueueConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PublisherDeadTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void publishDead(){
        rabbitTemplate.convertAndSend(DeadLetterQueueConfig.NORMAL_EXCHANGE, "normal.abc", "今天晚上有什么安排?");
        System.out.println("消息发送成功");
    }
}

执行生产者,发送消息到普通队列,等到10s后消息会发送放入到死信队列,如下:

RabbitMQ死信队列和延迟交换机_Test_07

1.4.3.设置Queue中的消息最大长度

消息超过队列中消息后,就会发送到死信队列,如下修改DeadLetterQueueConfig中队列设置如下:

RabbitMQ死信队列和延迟交换机_Test_08

上面设置,队列中消息超过2个后,就会发送到死信队列,调用之前的生产者,发送三条消息到队列中

RabbitMQ死信队列和延迟交换机_Test_09

二、延迟交换机

2.1.延迟交换机主要帮我们解决什么问题?

之前我们关于消息设置过期时间都是在消息本身以及队列的维度上来进行设置,这两个维度都在不同程度上有一些问题

  • 问题一:当我们的业务比较复杂的时候, 需要针对不同的业务消息类型设置不同的过期时间策略, 必然我们也需要为不同的队列消息的过期时间创建很多的Queue的Bean对象, 当业务复杂到一定程度时, 这种方式维护成本过高;
  • 问题二:就是队列的先进先出原则导致的问题,当先进入队列的消息的过期时间比后进入消息中的过期时间长的时候,消息是串行被消费的,所以必然是等到先进入队列的消息的过期时间结束, 后进入队列的消息的过期时间才会被监听,然而实际上这个消息早就过期了,这就导致了本来过期时间为3秒的消息,实际上过了13秒才会被处理,这在实际应用场景中肯定是不被允许的;

延迟交换机插件可以在一定程度上解决上述两种问题。

2.2. 延迟交换机插件的下载安装

2.2.1.延迟交换机插件下载地址

rabbitMQ插件下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases,如下:

RabbitMQ死信队列和延迟交换机_Test_10

2.2.2.复制到docker容器中rabbitMQ的安装目录下的插件目录中

将下载下来的文件,上传到服务器,然后拷贝到rabbitMQ容器中,如下:

docker cp rabbitmq_delayed_message_exchange-3.12.0.ez ea20d28661e1:/opt/rabbitmq/plugins

2.2.3.进入到rabbitMQ容器内部,启用延迟交换机插件

这里需要进入容器内部启动插件

# 进入容器
docker exec -it 容器id或者名称 /bin/bash

# 进入地址
cd /opt/rabbitmq/sbin

# 启动延迟交换机插件
./rabbitmq-plugins enable rabbitmq_delayed_message_exchange

执行如下图:

RabbitMQ死信队列和延迟交换机_Test_11

2.2.5.重启rabbitmq容器,更新配置

执行命令如下:

docker restart ea20d28661e1

2.2.6.验证

重启后,在页面添加交换机,会多出一个类型:

RabbitMQ死信队列和延迟交换机_spring_12

2.3. 延迟交换机使用演示代码

在config包下创建DelayedConfig ,其中创建延迟交换机,代码如下:

package com.augus.config;

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

import java.util.HashMap;

@Configuration
public class DelayedConfig {
    public static final String DELAYED_EXCHANGE = "delayed-exchange";
    public static final String DELAYED_QUEUE = "delayed-queue";
    public static final String DELAYED_ROUTING_KEY = "delayed.#";

    /**
     * 创建一个延迟交换机(Exchange)并返回该交换机对象。
     * @return
     */
    @Bean
    public Exchange delayedExchange(){
        //创建了一个HashMap对象,用于存储交换机的属性。然后,将一个名为x-delayed-type的属性和值为"topic"的键值对添加到HashMap中。
        HashMap<String, Object> map = new HashMap<>();
        map.put("x-delayed-type","topic");

        /**
         * 使用CustomExchange类创建一个自定义交换机对象。CustomExchange是Spring AMQP库提供的一个类,用于创建自定义的交换机。构造方法的参数依次为交换机的名称、类型、是否持久化、是否自动删除和属性。
         * ,交换机的名称为DELAYED_EXCHANGE,类型为"x-delayed-message",持久化为true,自动删除为false,属性为之前创建的HashMap对象。
         */
        return new CustomExchange(DELAYED_EXCHANGE,"x-delayed-message",true,false,map);
    }

    /**
     * 创建队列
     * @return
     */
    @Bean
    public Queue delayedQueue(){
        return QueueBuilder.durable(DELAYED_QUEUE).build();
    }

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

2.3. 创建生产者

创建生产者代码如下:

package com.augus;

import com.augus.config.DelayedConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class DelayedPublisherTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void publish(){
        rabbitTemplate.convertAndSend(DelayedConfig.DELAYED_EXCHANGE, "delayed.hello", "大江东去浪淘尽", new MessagePostProcessor() {
            //创建了一个匿名内部类实现了MessagePostProcessor接口,并重写了postProcessMessage()方法。在该方法中,设置了消息的延迟时间为50,000毫秒(即50秒)
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //设置消息的延迟时间,单位为毫秒。
                message.getMessageProperties().setDelay(5000);
                return message;
            }
        });
        System.out.println("消息发送成功");
    }
}

2.4. 测试

启动springboot项目,创建交换机和队列。然后启动生产者代码给发送消息到队列,由于上面设置了延迟时间是5000ms,所以发送后等待5000ms后才会发送到队列,如下:

RabbitMQ死信队列和延迟交换机_spring_13