RabbitMQ 延迟队列

1、延迟队列

所谓“延迟消息”是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费

2、使用场景

1、订单在十分钟之内未支付则自动取消
2、预定会议后,需要在预定时间点前十分钟通知各个与会人员参加会议。
3、淘宝七天自动确认收货,自动评价功能等

3、TTL(消息存活时间)

TTL 是 RabbitMQ 中一个消息或者队列的属性
表示一条消息或是该队列中的所有消息的最大存活时间,单位是毫秒;目前有两种方法可以设置消息的 TTL。
第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
第二种方法是对消息本身进行单独设置,每条消息的 TTL 可以不同。
如果两种方法一起使用,则消息的过期时间以两者之间较小的那个数值为准。消息在队列中的生存时间一旦超过设置的 TTL 值时,就会变成“死信”。

当设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列则会被丢到死信队列中)
当设置了消息的 TTL 属性,那么消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意一点是,如果不设置 TTL,表示消息永远不会过期

4、队列 TTL

可以使用x-message-ttl参数设置当前队列中所有消息的过期时间,即当前队列中所有的消息过期时间都一样;
在这里插入图片描述
建两个队列 QA 和 QB,两个队列 TTL 分别设置为 10s 和 30s,然后再创建一个交换机 X 和 死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如上图所示。

4.1、yml配置文件
# rabbitmq配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    listener:
      simple:
        concurrency: 1
        max-concurrency: 1
        acknowledge-mode: manual
        prefetch: 1
4.2、配置类
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 TtlQueueConfig {

    /**
     * 普通交换机名称
     */
    public static final String X_EXCHANGE = "X";

    /**
     * 死信交换机名称
     */
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";

    /**
     * 普通队列名称
     */
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";

    /**
     * 死信队列名称
     */
    public static final String DEAD_LETTER_QUEUE = "QD";

    /**
     * 声明 XExchange
     */
    @Bean
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }

    /**
     * 声明 yExchange
     */
    @Bean
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    /**
     * 声明队列 QA
     * 过期时间 10s
     */
    @Bean
    public Queue queueA(){
        Map<String, Object> arguments = new HashMap<>(3);
        // 设置死信交换机
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        // 设置死信路由键
        arguments.put("x-dead-letter-routing-key", "YD");
        // 设置过期时间
        arguments.put("x-message-ttl", 10000);
        return new Queue(QUEUE_A, true, false, false, arguments);
    }

    /**
     * 声明队列 QB
     * 过期时间 40s
     */
    @Bean
    public Queue queueB(){
        Map<String, Object> arguments = new HashMap<>(3);
        // 设置死信交换机
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        // 设置死信路由键
        arguments.put("x-dead-letter-routing-key", "YD");
        // 设置过期时间
        arguments.put("x-message-ttl", 40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
    }

    /**
     * 死信队列QD
     */
    @Bean
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }


    /**
     * 队列 A 与 X交换机绑定
     */
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    /**
     * 队列 B 与 X交换机绑定
     */
    @Bean
    public Binding queueBBindingX(){
        return new Binding(QUEUE_B, Binding.DestinationType.QUEUE, X_EXCHANGE, "XB", null);
    }

    /**
     * 队列 D 与 y交换机(死信交换机)绑定
     */
    @Bean
    public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }

}

4.3、生产者
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;

@RestController
@RequestMapping("/ttl")
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("/sendMsg/{message}")
    public String sendMsg(@PathVariable String message){
        System.out.println("当前时间:"+ new Date() +"发送一条消息"+ message +"给两个队列");
        rabbitTemplate.convertAndSend("X", "XA", "消息来自TTL为10s队列QA:"+message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自TTL为40s队列QB:"+message);
        return "发送成功";
    }
}
4.4、消费者
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.Date;

@Component
public class DeadLetterConsumer {

    @RabbitListener(queues = "QD")
    public void receiveD(String msgData, Message message, Channel channel) throws IOException {
        //手动确认
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        System.out.println("当前时间"+ new Date() +",收到死信队列的消息:"+msgData);
    }
}
4.5、结果

在这里插入图片描述
QA消息在 10s 后变成了死信消息,然后被消费者消费掉了,
QB消息在 30s 后变成了死信消息,然后被消费掉,这样一个延时队列就完成了。

5、消息 TTL

单独给某条消息设置ttl(生产环境中居多)
可以使用Expiration参数来设置单个消息的过期时间。当时间一到就会被移出队列。
一次设置中我们同时设置了两种消息过期方式和的话,时间短的会优先触发,比如,我们给某个队列设置过期时间为10秒,所有发到这个队列的消息10秒内未被消费都会过期,我们同时又给某个消息设置过期时间为5秒,则在消息发送到队列中5秒后就会过期。
在这里插入图片描述
新增了一个队列 QC,该队列不设置 TTL 时间,绑定关系如上图所示

5.1、配置类新增代码
 public static final String QUEUE_C = "QC";
 /**
 * 死信队列 QC
  */
 @Bean
 public Queue queueC(){
     Map<String, Object> arguments = new HashMap<>(2);
     // 声明当前队列绑定的死信交换机
     arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
     // 声明当前队列的死信路由key
     arguments.put("x-dead-letter-routing-key", "YD");
     return new Queue(QUEUE_C, false, false, false, arguments);
 }
   /**
  * 队列 c 与 X交换机绑定
  */
 @Bean
 public Binding queueCBindingX(){
     return new Binding(QUEUE_C, Binding.DestinationType.QUEUE, X_EXCHANGE, "XC", null);
 }
5.2、生产者新增方法
@RequestMapping("/sendMsg1/{message}/{ttlTime}")
public String sendMsg1(@PathVariable String message, @PathVariable String ttlTime){
    MessagePostProcessor messagePostProcessor = message1 -> {
        message1.getMessageProperties().setExpiration(ttlTime);
        return message1;
    };
    System.out.println("当前时间:"+ new Date() +"发送一条消息("+ ttlTime +"毫秒过期)给QC队列:"+message);
    rabbitTemplate.convertAndSend("X", "XC", "消息来自队列QC:"+message, messagePostProcessor);
    return "发送成功";
}
5.3、测试结果

在这里插入图片描述
两条消息的过期时间不一致,过期时间短的那条消息,在过期时间到了以后并没有立即被消费,而是和过期时间长的那条消息一起被消费了。
所以,如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡”,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先被执行。

6、Rabbitmq 插件实现延迟队列

rabbitMq版本 3.10.9
Erlang 25.0
延时消息交换机插件 3.10.2

6.1、rabbitmq安装参考文章 https://blog.csdn.net/weixin_45486926/article/details/127170831?spm=1001.2014.3001.5502
6.2、延时消息交换机插件下载地址 https://www.rabbitmq.com/community-plugins.html
6.3、开启延时消息交换机插件

将插件放到RabbitMQ安装目录的plugins文件中
在RabbitMQ 安装目的sbin用cmd使用如下命令安装插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

在这里插入图片描述
开启插件后,启动RabbitMQ,访问登录后访问http://localhost:15672,用guest/guest登录后
在交换机exchanges的tab下,底部新增将看到下图,则表示插件已启动,就可以使用了。
在这里插入图片描述

6.4、延时插件实现延时队列

在这里插入图片描述

创建一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如上图所示

6.4.1、配置类
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;

@Configuration
public class DelayedConfig {

    /**
     * 延时交换机名称
     */
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";

    /**
     * 普通队列名称
     */
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";

    /**
     * 延时路由key
     */
    public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";

    /**
     * 死信队列QD
     */
    @Bean
    public Queue delayedQueue(){
        return new Queue(DELAYED_QUEUE_NAME, false, false, false, null);
    }

    /**
     * 定义延迟交换机
     * 需要死信交换机和死信队列,支持消息延迟投递,消息投递之后没有到达投递时间,是不会投递给队列
     * 而是存储在一个分布式表,当投递时间到达,才会投递到目标队列
     * @return
     */
    @Bean
    public CustomExchange delayedExchange(){
        Map<String, Object> args = new HashMap<>(1);
        args.put("x-delayed-type", "direct");//交换机的类型
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
    }

    /**
     * 队列与交换机绑定
     */
    @Bean
    public Binding queueABindingX(Queue delayedQueue, CustomExchange  delayedExchange){
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}
6.4.2、生产者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;

@Slf4j
@RestController
@RequestMapping("/delayed")
public class SendMessageController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";

    @RequestMapping("/sendMsg/{message}/{delayTime}")
    public String sendMsg(@PathVariable String message, @PathVariable Integer delayTime){
        rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, messagePostProcessor ->{
            messagePostProcessor.getMessageProperties().setDelay(delayTime);
            return messagePostProcessor;
        });
        log.info("当前时间:{},发送一条延迟{}毫秒的信息给队列delay.queue:{}", new Date(), delayTime, message);
        return "发送成功";
    }
}
6.4.3、消费者
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
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.Date;

@Slf4j
@Component
public class DeadLetterConsumer {

    public static final String DELAYED_QUEUE_NAME = "delayed.queue";

    @RabbitListener(queues = DELAYED_QUEUE_NAME)
    public void receiveDelayedQueue(Message message, Channel channel) throws IOException {
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//手动消息确认
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到延时队列的消息:{}", new Date(), msg);
    }
}
6.4.4、测试结果

延时短的消息被先消费掉了,符合预期结果
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

古口古

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值