走进RabbitMQ高频面试点TTL、死信队列、延迟队列

101 阅读6分钟

TTL

设置队列参数、交换机参数、发消息都可以用页面。

也能用代码。

TTL 全称 Time To Live(存活时间/过期时间)。当消息到达存活时间后,还没有被消费,会被自动清除。

RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

1569166173852

可以在RabbitMQ管理控制台设置过期时间,此处不做重点讲解。

一起做一下

创建队列 test_queue_ttl 设置ttl为10秒

image-20200321000505022

image-20200321000505022

创建交换机test_exchange_ttl 绑定 队列

image-20200321000710192

image-20220714141856963

向交换机发一个消息

image-20220714142043712

查看队列消息,过10秒删除

image-20220714142021614

(1)代码实现

a、设置队列的过期时间
  1. 在消息的生产方中,在 spring-rabbitmq-producer.xml 配置文件中,添加如下配置:

    <!--ttl-->
    <rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
        <!--设置queue的参数-->
        <rabbit:queue-arguments>
            <!--x-message-ttl指队列的过期时间-->
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>
    
    <rabbit:topic-exchange name="test_exchange_ttl" >
        <rabbit:bindings>
            <rabbit:binding pattern="tang.#" queue="test_queue_ttl"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>
    
  2. 编写发送消息测试方法

        @Test
        public void ttlTest(){
            for (int i = 0; i < 10; i++) {
                rabbitTemplate.convertAndSend("test_exchange_ttl","tang.haha","hello ttl"+i);
            }
        }
    
  3. 测试结果:当消息发送成功后,过10s后在RabbitMQ的管理控制台会看到消息会自动删除。

image-20220714142521691

image-20220714142521691

b、设置单个消息的过期时间

编写代码测试,并且设置队列的过期时间为10s, 单个消息的过期时间为5s

    @Test
    public void testTtl2() {

        // 消息后处理对象,设置一些消息的参数信息
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //1.设置message的信息
                message.getMessageProperties().setExpiration("5000");//消息的过期时间
                //2.返回该消息
                return message;
            }
        };

        //消息单独过期
        rabbitTemplate.convertAndSend("test_exchange_ttl",
                "ttl.hehe", "message ttl....",messagePostProcessor);

        for (int i = 0; i < 10; i++) {
            if(i == 5){
                //消息单独过期
                rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor);
            }else{
                //不过期的消息
                rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....");

            }
        }
    }

如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。

  • 队列过期后,会将队列所有消息全部移除。
  • 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)

5s

image-20220714143016960

20s

image-20220714143257013

5 10 5 10

image-20220714143611629

(2)小结

  • 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
  • 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
  • 如果两者都进行了设置,以时间短的为准。

死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。

1569167524589

消息成为死信的三种情况:

  1. 队列消息长度到达限制;
  2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
  3. 原队列存在消息过期设置,消息到达超时时间未被消费;

队列绑定死信交换机:

给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key

1569167616750

(1)代码实现

  1. 在消息的生产方中,在 spring-rabbitmq-producer.xml 配置文件中,添加如下配置:

    • 声明正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)

      <rabbit:queue name="test_queue_dlx" id="test_queue_dlx">
      </rabbit:queue>
      <rabbit:topic-exchange name="test_exchange_dlx">
          <rabbit:bindings>
              <rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"></rabbit:binding>
          </rabbit:bindings>
      </rabbit:topic-exchange>
      
    • 声明死信队列(queue_dlx)和死信交换机(exchange_dlx)

      <rabbit:queue name="queue_dlx" id="queue_dlx"></rabbit:queue>
      <rabbit:topic-exchange name="exchange_dlx">
          <rabbit:bindings>
              <rabbit:binding pattern="dlx.#" queue="queue_dlx"></rabbit:binding>
          </rabbit:bindings>
      </rabbit:topic-exchange>
      
    • 正常队列绑定死信交换机,并设置相关参数信息

      <rabbit:queue name="test_queue_dlx" id="test_queue_dlx">
          <!--3. 正常队列绑定死信交换机-->
          <rabbit:queue-arguments>
              <!--3.1 x-dead-letter-exchange:死信交换机名称-->
              <entry key="x-dead-letter-exchange" value="exchange_dlx" />
      
              <!--3.2 x-dead-letter-routing-key:发送给死信交换机的routingkey-->
              <entry key="x-dead-letter-routing-key" value="dlx.hehe" />
      
              <!--4.1 设置队列的过期时间 ttl-->
              <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer" />
              <!--4.2 设置队列的长度限制 max-length -->
              <entry key="x-max-length" value="10" value-type="java.lang.Integer" />
          </rabbit:queue-arguments>
      </rabbit:queue>
      <rabbit:topic-exchange name="test_exchange_dlx">
          <rabbit:bindings>
              <rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"></rabbit:binding>
          </rabbit:bindings>
      </rabbit:topic-exchange>
      
  2. 编写测试方法

        /**
         * 发送测试死信消息:
         *  1. 过期时间
         *  2. 长度限制
         *  3. 消息拒收
         */
        @Test
        public void testDlx(){
            //1. 测试过期时间,死信消息
            rabbitTemplate.convertAndSend("test_exchange_dlx",
                                          "test.dlx.haha","我是一条消息,我会死吗?");
    
            //2. 测试长度限制后,消息死信
           for (int i = 0; i < 20; i++) {
                rabbitTemplate.convertAndSend("test_exchange_dlx",
                                              "test.dlx.haha","我是一条消息,我会死吗?");
            }
    
            //3. 测试消息拒收
            rabbitTemplate.convertAndSend("test_exchange_dlx",
                                          "test.dlx.haha","我是一条消息,我会死吗?");
    
        }
    

3消息拒绝接受消费者增加监听

package com.tang.listener;



import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * Consumer ACK机制:
 *  1. 设置手动签收。acknowledge="manual"
 *  2. 让监听器类实现ChannelAwareMessageListener接口
 *  3. 如果消息成功处理,则调用channel的 basicAck()签收
 *  4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
 */
@Component
public class DlxListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2. 处理业务逻辑
            System.out.println("处理业务逻辑...");
            int i = 3/0;//出现错误
            //3. 手动签收
            channel.basicAck(deliveryTag,true);
        } catch (Exception e) {
            //e.printStackTrace();
            System.out.println("拒绝接受");
            //4.拒绝签收
            /*
            第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag,true,false);
            // 了解
            //channel.basicReject(deliveryTag,true);
        }
    }
}

配置文件

 <rabbit:listener ref="dlxListener" queue-names="test_queue_dlx"/>

(2)小结

  1. 死信交换机和死信队列和普通的没有区别

  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

  3. 消息成为死信的三种情况:

    • 队列消息长度到达限制;
    • 消费者拒接消费消息,并且不重回队列;
    • 原队列存在消息过期设置,消息到达超时时间未被消费;

6、延迟队列-重点

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

提出需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存。
  2. 新用户注册成功7天后,发送短信问候。

实现方式:

  1. 定时器(不优雅!)
  2. 延迟队列

1569168202661

注意:在RabbitMQ中并未提供延迟队列功能。

但是可以使用:TTL+死信队列 组合实现延迟队列的效果。

1569168255196

(1)代码实现

  1. 在消息的生产方中,在 spring-rabbitmq-producer.xml 配置文件中,添加如下配置:

    <!-- 1. 定义正常交换机(order_exchange)和队列(order_queue)-->
    <rabbit:queue id="order_queue" name="order_queue">
        <!-- 3. 绑定,设置正常队列过期时间为30分钟-->
        <rabbit:queue-arguments>
            <entry key="x-dead-letter-exchange" value="order_exchange_dlx" />
            <entry key="x-dead-letter-routing-key" value="dlx.order.cancel" />
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer" />
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="order_exchange">
        <rabbit:bindings>
            <rabbit:binding pattern="order.#" queue="order_queue"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>
    
    <!--  2. 定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)-->
    <rabbit:queue id="order_queue_dlx" name="order_queue_dlx"></rabbit:queue>
    <rabbit:topic-exchange name="order_exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.order.#" queue="order_queue_dlx"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>
    
  2. 编写测试方法

    @Test
    public  void testDelay() throws InterruptedException {
        //1.发送订单消息。 将来是在订单系统中,下单成功后,发送消息
        rabbitTemplate.convertAndSend("order_exchange",
                                      "order.msg","订单信息:id=1,time=2022年");
    
        //2.打印倒计时10秒
        for (int i = 10; i > 0 ; i--) {
            System.out.println(i+"...");
            Thread.sleep(1000);
        }
    }
    

3消费者

package com.tang.listener;


import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * Consumer ACK机制:
 *  1. 设置手动签收。acknowledge="manual"
 *  2. 让监听器类实现ChannelAwareMessageListener接口
 *  3. 如果消息成功处理,则调用channel的 basicAck()签收
 *  4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
 */
@Component
public class OrderListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2. 处理业务逻辑
            System.out.println("处理业务逻辑...");
            System.out.println("查询数据库。。。");
            System.out.println("查看支付状态。。。");

            //3. 手动签收
            channel.basicAck(deliveryTag,true);
        } catch (Exception e) {
            //e.printStackTrace();
            System.out.println("拒绝接受");
            //4.拒绝签收
            /*
            第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag,true,false);
            // 了解
            //channel.basicReject(deliveryTag,true);
        }
    }
}

配置文件

<rabbit:listener ref="orderListener" queue-names="order_queue_dlx"/>

(2) 小结

  1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
  2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。