RabbitMq如何实现延迟队列

331 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

简介

延迟队列存储的对象是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

常用场景

  • 订单在十分钟之内未支付则自动取消
  • 用户注册成功后,如果三天内没有登录则进行短信提醒
  • 用户发起退款后,如果三天内没有得到处理则通知相关运营人员
  • 用户下单外卖以后,距离超时时间还有 10 分钟时提醒外卖小哥即将超时
  • .....

RabbitMq实现延迟队列方案

RabbitMq实现延迟队列的方案有如下两种:

  • 第一种:DLX+TTL(Time To Live); 设置TTL分为两种:在队列属性中设置TTL,或者消息属性中设置TTL
  • 第二种:使用延时队列插件

DLX+TTL实现延时队列

DLX全称(Dead-Letter-Exchange)称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。

TTL全称(Time To Live)称为过期时间,TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。

实现原理

延迟队列为常见的使用模式,如下图所示,生产者产生的消息首先会进入缓冲队列(图中红色队列)。通过RabbitMQ提供的TTL扩展,由于这些消息设置过期时间,等消息过期之后,这些消息会通过配置好的DLX转发到实际消费队列(图中蓝色队列),以此达到延迟消费的效果。

图片.png

具体实现

死信队列配置

@Configuration
public class DeadLetterConfig
{
    /**
     * 普通队列
     */
    public static final String TTL_EXCHANGE_NAME = "ttl_exchange"; 
    
    public static final String TTL_ROUTING_KEY = "ttl_routing_key"; 
    
    public static final String TTL_QUEUE_NAME = "ttl_queue"; 
    
    /**
     * 死信队列名称
     */
    public static final String DLX_QUEUE_NAME = "dlx_queue"; 
    /**
     * 死信交换机
     */
    public static final String DLX_EXCHANGE_NAME = "dlx_exchange"; 
    
    /**
     * 死信绑定Key
     */
    public static final String DLX_ROUTING_KEY = "dlx_routing_key";

    
    /** 
     * 死信交换机 
     * @return 
     */ 
    @Bean 
    DirectExchange dlxExchange() { 
        return new DirectExchange(DLX_EXCHANGE_NAME, true, false); 
    } 
    
    /** 
     * 死信队列 
     * @return 
     */ 
    @Bean
    Queue dlxQueue() { 
        return new Queue(DLX_QUEUE_NAME, true, false, false); 
    } 
 
 
    /** 
     * 绑定死信队列和死信交换机 
     * @return 
     */ 
    
    @Bean 
    Binding dlxBinding() { 
        return BindingBuilder.bind(dlxQueue()) 
                .to(dlxExchange()) 
                .with(DLX_ROUTING_KEY); 
    } 
    
    /** 
     * 普通交换机 
     * @return 
     */ 
    @Bean 
    DirectExchange ttlExchange() { 
        return new DirectExchange(TTL_EXCHANGE_NAME, true, false); 
    } 
 
    /** 
     * 普通消息队列 
     * @return 
     */ 
    @Bean
    Queue ttlQueue() 
    { 
        Map<String, Object> args = new HashMap<>(3); 
        //设置死信交换机 
        args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME); 
        //设置死信 routing_key 
        args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
        //设置消息过期时间 为10秒
        args.put("x-message-ttl", 10000); 
        return QueueBuilder.durable(TTL_QUEUE_NAME).withArguments(args).build();
    } 
 
 
    /** 
     * 普通队列通过js_routing_key绑定到死信交换机
     * @return 
     */ 
    @Bean 
    Binding jsBinding() { 
        return BindingBuilder.bind(ttlQueue()) 
                .to(ttlExchange()) 
                .with(TTL_ROUTING_KEY); 
    } 
}

消息发送者

@Component
public class DeadLetterProduce
{
    private Logger logger =LoggerFactory.getLogger(DelayedMqProduce.class);
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void sendMessage(String exchange, String routeKey, String message)
    {
            String msg = LocalDateTime.now().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))+",message:"+message;
        
        logger.info("send dead letter time:{}",msg);
        rabbitTemplate.convertAndSend(exchange, routeKey, msg);
    }
}

说明:消息和队列都设置了过期时间,则获取最小的时间为准。

消息接收者

@Component
@RabbitListener(queues = "dlx_queue")
public class DeadLetterMqConsumer
{
    private static final Logger logger = LoggerFactory.getLogger(DeadLetterMqConsumer.class);
    
    @RabbitHandler
    public void receive(String message) 
    {
       logger.info("发送消息的时间:{}",message);
        logger.info("消费者收到延迟消息时间:"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

说明:注意监听的队列名称为死信队列名称。

测试

@RequestMapping("/sendDeadLetterMessage")
    public String sendDeadLetterMessage()
    {
        deadLetterProduce.sendMessage("ttl_exchange", "ttl_routing_key", "this is dead letter message");
        return "success";
    }

图片.png

小结

由于消息采用先进先出的原则,采用此种方案,由于MQ只会检查第一个消息是否过期,如果第一个消息延迟很长时间,会导致第二个消息不会被优先执行,所以需要采用RabbitMq的延迟消息插件来解决此问题。

延时队列插件

Windows安装Rabbitmq安装延时插件

下载延时插件

github.com/rabbitmq/ra…

图片.png

说明:注意插件的版本与Rabbitmq的版本对应。

将延时插件拷贝到Rabbitmq的插件目录

图片.png

安装延时插件

进入Rabbitmq的目录的sbin目录,执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange安装插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

图片.png

查看插件是否安装成功

登录Rabbitmq后台管理,查询Exchange的类型出现x-delayed-message类型说明安装成功。

图片.png

实现延迟队列

配置延迟消息

@Configuration
public class DelayedConfig
{
    private final String QUEUE_NAME="delayed_queue";
    
    private final String EXCHANGE_NAME="delay_exchange";

    @Bean
    public Queue delayedQueue() 
    {
        return new Queue(QUEUE_NAME,true);
    }
    
    @Bean
    CustomExchange customExchange() {
        Map<String, Object> args = new HashMap<>();
        /
        args.put("x-delayed-type", "direct");
        //参数二为类型:必须是x-delayed-message
        return new CustomExchange(EXCHANGE_NAME, "x-delayed-message", true, false, args);
    }
    
    @Bean
    Binding binding(Queue queue, CustomExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(QUEUE_NAME).noargs();
    }
}

消息发送者

@Component
public class DelayedMqProduce
{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void sendMessage(String exchangeName,String queueName,String message,long delayTime)
    {
        rabbitTemplate.convertAndSend(exchangeName,queueName,message,new MessagePostProcessor()
        {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException
            {
                message.getMessageProperties().setHeader("x-delay",delayTime);
                return message;
            }
        });
    }
}

消息接收者

@Component
@RabbitListener(queues = "delayed_queue")
public class DelayedMqConsumer
{
    private static final Logger logger = LoggerFactory.getLogger(DelayedMqConsumer.class);
    
    @RabbitHandler
    public void receive(String message) 
    {
        logger.info("receive delayedmessage content:{}",message);
    }
}

测试结果

2022-08-14 22:53:29.061 [http-nio-9090-exec-4] INFO  [] c.s.f.r.produce.DelayedMqProduce - 当 前 时 间 : Sat Aug 13 22:53:29 CST 2022, 发送延迟 3000毫秒的信息给队列 delayed.queue:(Body:'this is delayed message' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=23, deliveryMode=PERSISTENT, priority=0, deliveryTag=0])

2022-08-14 22:53:32.070 [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] INFO  [] c.s.f.r.consumer.DelayedMqConsumer - receive delayedmessage content:this is delayed message

总结

本文讲解了Rabbitmq实现延迟队列的两种方案,个人建议采用插件的方式来实现延迟队列,如有疑问,请随时反馈。