Bootstrap

RabbitMQ高级特性第3篇-TTL和死信队列

一、TTL概述

  • TTL 全称 Time To Live (存活时间/过期时间)。
  • 当消息到达存活时间后,还没有被消费,就会被自动删除。
  • RabbitMQ可以对单个消息设置过期时间,也可以对整个队列设置过期时间。
比如:当用户下单之后30分钟不支付就会失效:订单系统发送一个消息到MQ中30分钟后用户还没有支付就不能再支付了,此时要让订单失效

 

二、设置单个消息过期时间 

  • 发送消息时针对单个消息设置过期时间,代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRabbitMQ {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 发送消息到mq并设置过期时间
    @Test
    public void testSendStr(){
        // 发送消息
        // 参数1:交换机名字
        // 参数2:路由键
        // 参数3:消息字符串
        rabbitTemplate.convertAndSend(
                "xkp_topic_exchange",
                "xkp.news",
                "hello rabbitmq111......",message -> {
                    // 设置消息过期时间:单位:毫秒
                    message.getMessageProperties().setExpiration("10000");
                    // 返回消息对象
                    return message;
                });
    }
}

三、设置整个队列过期时间

方式1:在消息生产方创建队列时设置

package com.xkp.config;

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

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

@Configuration
public class RabbitMQConfig {
    // 交换机名称
    public static final String EXCHANGE_NAME = "xkp_topic_exchange";
    // 队列名称
    public static final String QUEUE_NAME = "xkp_queue";

    // 1. 定义交换机
    @Bean("xkpExchange")
    public Exchange createExchange(){
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    // 2. 定义队列
    @Bean("xkpQueue")
    public Queue createQueue(){
        // 创建map集合:封装队列参数
        Map<String,Object> map = new HashMap<>();
        // 设置队列过期时间
        map.put("x-message-ttl", 20000);
        return QueueBuilder.durable(QUEUE_NAME).withArguments(map).build();
    }

    // 3. 队列与交换机绑定关系
    @Bean
    public Binding bindExchangeAndQueue(@Qualifier("xkpQueue") Queue queue,
                                        @Qualifier("xkpExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("xkp.#").noargs();
    }

}

 

方式2:在消息消费方监听消息时设置

@Component
public class RabbitMQConfig {
    /**
     * 监听消息,绑定队列设置队列过期时间
     */
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = "xkp_topic_exchange",type = "topic"),
            value = @Queue(
                    value = "xkp_queue",
                    durable = "true",
                    arguments = @Argument( // 设置队列参数
                            name="x-message-ttl", // 设置队列过期时间
                            type = "java.lang.Integer",
                            value = "20000")),
            key = "xkp.#"
    ))
    public void handlerMessage(Message message, Channel channel)throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        // 1. 转换消息
        System.out.println("消息内容: " + new String(message.getBody()));
        // 2. 手工签收
        channel.basicAck(deliveryTag,true);
    }
}
  • 注意事项
    • 一旦设置队列过期时间,推荐在消费方和生产方同时指定且值要相同,否则后启动的一方会出现错误。
    • 如果设置了队列过期时间也设置了消息的过期时间则以时间短的为准

四、死信队列

1、什么是死信队列

死信队列英文全称:Dead Letter Exchange,英文缩写:DLX。中文名称:死信交换机,由于其他消息中间件没有交换机概念,只有RabbitMQ中有交换机的概念,所以习惯称死信交换机为死信队列。

当消息成为Dead message 后可以重新发送到另一个交换机,这个交换机就是DLX。

2、消息成为死信的三种情况

  1. 队列消息数量达到限制
  2. 消费者拒接接收消息:消费者调用basicNack/basicReject方法并且不把消息重新加入原目标队列,即requeue=false;
  3. 原队列设置了过期设置,消息到达过期时间未被消费

3、队列绑定死信交互机

 死信交换机的定义和普通交换机的定义完全相同,队列绑定死信交换机与绑定普通交换机的方式完全相同,死信交换机就是一个普通的交换机,只是换了一个叫法而已,没有什么特殊之处,给队列绑定死信交换机需要给队列设置如下两个参数:

  • x-dead-letter-exchange          死信交换机名称
  • x-dead-letter-routing-key      发送给死信交换机的routingKey

4、实现代码

1、定义普通交换机和普通队列

2、定义死信交换机和死信队列

3、普通队列绑定死信交换机


@Configuration
public class RabbitMQConfig {
    // 普通交换机名称
    public static final String EXCHANGE_NAME = "xkp_topic_exchange";
    // 死信交换机名称
    public static final String DEAD_EXCHANGE_NAME = "dead_xkp_topic_exchange";

    // 普通队列名称
    public static final String QUEUE_NAME = "xkp_queue";

    // 死信队列名称
    public static final String DEAD_QUEUE_NAME = "dead_xkp_queue";

    // 1. 定义普通交换机
    @Bean("xkpExchange")
    public Exchange createExchange(){
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    // 2. 定义普通队列
    @Bean("xkpQueue")
    public Queue createQueue(){
        // 创建map集合:封装队列参数
        Map<String,Object> map = new HashMap<>();
        // 绑定死信交换机名称
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        // 设置发送给死信交换机的routingKey
        map.put("x-dead-letter-routing-key", "dead.xkp.news");
        // 设置队列过期时间
        map.put("x-message-ttl", 20000);
        // 设置队列可以存储的最大消息数量
        map.put("x-max-length", 10);
        return QueueBuilder.durable(QUEUE_NAME).withArguments(map).build();
    }

    // 3. 定义死信交换机
    @Bean("deadXkpExchange")
    public Exchange createDeadExchange(){
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE_NAME).durable(true).build();
    }

    // 4. 定义死信队列
    @Bean("deadXkpQueue")
    public Queue createDeadQueue(){
        // 创建map集合:封装队列参数
        return QueueBuilder.durable(DEAD_QUEUE_NAME).build();
    }

    // 5. 普通队列与普通交换机绑定关系
    @Bean
    public Binding bindExchangeAndQueue(@Qualifier("xkpQueue") Queue queue,
                                        @Qualifier("xkpExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("xkp.#").noargs();
    }

    // 6. 死信队列与死信交换机绑定关系
    @Bean
    public Binding bindDeadExchangeAndQueue(@Qualifier("deadXkpQueue") Queue queue,
                                        @Qualifier("deadXkpExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("dead.xkp.#").noargs();
    }

}

4、在消息生产方编写测试类

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

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 测试死信消息
     */
    @Test
    public void testDLX(){
        // 测试过期时间:过期没有消费的消息会进入死信队列
        rabbitTemplate.convertAndSend(
                    "xkp_topic_exchange",
                    "xkp.news",
                    "hello rabbitmq 我是死信消息......" + i);

        // 测试消息数量达到限制:超过数量的消息会进入死信队列
        for (int i = 0; i < 20 ; i++) {
            // 发送消息
            rabbitTemplate.convertAndSend(
                    "xkp_topic_exchange",
                    "xkp.news",
                    "hello rabbitmq 我是死信消息......" + i);
        }

        // 测试消息拒收:拒收消息会进入死信队列
        rabbitTemplate.convertAndSend(
                    "xkp_topic_exchange",
                    "xkp.news",
                    "hello rabbitmq 我是死信消息......" + i);
    }
}

5、在消息消费方编写监听消息方法:模拟异常然后拒绝签收消息

@Component
public class RabbitMQConfig {
   
    /**
    * 测试拒绝签收消息,让消息进入死信队列                           
    **/
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = "xkp_topic_exchange",type = "topic"),
            value = @Queue(value = "xkp_queue",durable = "true"),
            key = "xkp.#"
    ))
    public void handlerMessage(Message message, Channel channel)throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 1. 转换消息
            System.out.println("消息内容: " + new String(message.getBody()));
            // 2. 业务处理
            System.out.println("执行业务处理...");
            // 模拟异常
            System.out.println(1/0);
            // 3. 手工签收
            channel.basicAck(deliveryTag,true);

        } catch (Exception e) {
            // 4. 拒绝签收
            // 参数3:true:重回队列,broker会重新发送该消息给消费端,false:不重回队列,消息会被丢弃或回到死信队列
            channel.basicNack(deliveryTag,true,false);
        }
    }
    
}

 

;