RabbitMQ-延迟队列

延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理。简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列

使用场景

  • 订单在十分钟之内未支付则自动取消
  • 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒
  • 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  • 用户发起退款,如果三天内没有得到处理则通知相关运营人员
  • 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

TTL

  • TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒
  • 如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"
  • 如果同时配置了队列的 TTL 和消息的TTL,那么较小的那个值将会被使用

消息设置TTL

java 复制代码
rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
	//设置消息过期时间
	correlationData.getMessageProperties().setExpiration("10");
	return correlationData;
});

队列设置TTL

java 复制代码
public Queue queueA(){
	Map<String, Object> args = new HashMap<>(3);
	//声明当前队列绑定的死信交换机
	args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
	//声明当前队列的死信路由 key
	args.put("x-dead-letter-routing-key", "YD");
	//声明队列的 TTL
	args.put("x-message-ttl", 10000);
	return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
  1. 两种TTL区别
  • 队列的TTL,消息一旦过期就会被队列丢弃(如果配置了死信队列被丢到死信队列中)
  • 消息的TTL,即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间
  • 如果消息不设置TTL,表示永远不会过期,如果将TTL设置为0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃

队列TTL

  • SpringBoot配置文件-15672是Web管理界面的端口;5672是MQ访问的端口

    spring.rabbitmq.host=192.168.1.128
    spring.rabbitmq.port=5672
    spring.rabbitmq.username=admin
    spring.rabbitmq.password=123
    spring.rabbitmq.virtual-host=/

  • 队列配置类

java 复制代码
@Configuration
public class TtlQueueConfig {
    
    public static final String X_EXCHANGE = "X";
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    public static final String DEAD_LETTER_QUEUE = "QD";
    
    // 声明 xExchange
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }
    
    // 声明 xExchange
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }
    
    //声明队列 A ttl 为 10s 并绑定到对应的死信交换机
    @Bean("queueA")
    public Queue queueA(){
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列的 TTL
        args.put("x-message-ttl", 10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
    }
    // 声明队列 A 绑定 X 交换机
    @Bean
    public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }
    
    //声明队列 B ttl 为 40s 并绑定到对应的死信交换机
    @Bean("queueB")
    public Queue queueB(){
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列的 TTL
        args.put("x-message-ttl", 40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
    }
    
    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
    }
    
    //声明死信队列 QD
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }
    
    //声明死信队列 QD 绑定关系
    @Bean
    public Binding deadLetterBindingQAD(
            @Qualifier("queueD") Queue queueD,
            @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}
  • 生产者
java 复制代码
@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @GetMapping("sendMsg/{message}")
    public void sendMsg(@PathVariable String message){
        log.info("当前时间: {},发送一条信息给两个 TTL 队列:{}", new Date(), message);
        rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
    }
}
  • 消费者
java 复制代码
@Slf4j
@Component
public class DeadLetterQueueConsumer {
    
    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        log.info("当前时间: {},收到死信队列信息{}", new Date().toString(), msg);
    }
}

延时队列优化之消息设置TTL

  • 上述例子,每增加一个新的时间需求,就要新增一个队列,现在只有 10S 和 40S两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列
  • 优化成一个队列适用于所有延时场景,增加队列QC
  • QC配置类
java 复制代码
@Component
public class MsgTtlQueueConfig {
    
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    public static final String QUEUE_C = "QC";
    
    //声明队列 C 死信交换机
    @Bean("queueC")
    public Queue queueC() {
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //没有声明 TTL 属性
        return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
    }
    
    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }
    
}
  • 生产者
java 复制代码
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
	rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
		correlationData.getMessageProperties().setExpiration(ttlTime);
		return correlationData;
	});
	log.info("当前时间: {},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}
  • 缺陷 :如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时"死亡",因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行

延时插件

  • 为解决延时时间短的消息先被消费执行,可以使用延时插件

  • 插件下载地址:https://www.rabbitmq.com/community-plugins.html

  • 延迟插件名:rabbitmq_delayed_message_exchange

  • 将插件放置到RabbitMQ安装目录的plugins 目录下

    cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins

  • 开启插件

    rabbitmq-plugins enable rabbitmq_delayed_message_exchange

  • 重启mq,查看交换机类型,增加了x-delayed-message

    systemctl restart rabbitmq-server

插件案例

  • 新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange
  • 配置类
java 复制代码
@Configuration
public class DelayedQueueConfig {
    
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
   
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }
    
    //自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机的类型
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME,
                "x-delayed-message",
                true, //是否需要持久化
                false, //是否需要自动删除
                args); //其余参数
    }
    
    @Bean
    public Binding bindingDelayedQueue(
            @Qualifier("delayedQueue") Queue queue,
            @Qualifier("delayedExchange") CustomExchange delayedExchange) {
        return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}
  • 生产者
java 复制代码
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
	rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message,
			correlationData ->{
                //发送消息的时候 延长的时间 单位ms
				correlationData.getMessageProperties().setDelay(delayTime);
				return correlationData;
			}
	);
	
	log.info(" 当 前 时 间 : {}, 发 送 一 条 延 迟 {} 毫 秒 的 信 息 给 队 列 delayed.queue:{}",
			new Date(),delayTime, message);
}
  • 消费者
java 复制代码
public static final String DELAYED_QUEUE_NAME = "delayed.queue";

@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){
	String msg = new String(message.getBody());
	log.info("当前时间: {},收到延时队列的消息: {}", new Date().toString(), msg);
}
  • 查看效果
相关推荐
回家路上绕了弯13 小时前
深入解析Agent Subagent架构:原理、协同逻辑与实战落地指南
分布式·后端
用户83071968408216 小时前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
用户8307196840823 天前
RabbitMQ vs RocketMQ 事务大对决:一个在“裸奔”,一个在“开挂”?
后端·rabbitmq·rocketmq
初次攀爬者4 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者6 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧7 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖7 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农7 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者7 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀7 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式