延迟队列

  延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。

  场景:

  • 延迟消费。比如:
    • 用户生成订单之后,需要过一段时间校验订单的支付状态,如果订单仍未支付则需要及时地关闭订单。
    • 用户注册成功之后,需要过一段时间比如一周后校验用户的使用情况,如果发现用户活跃度较低,则发送邮件或者短信来提醒用户使用。
  • 延迟重试。比如消费者从队列里消费消息时失败,但是想要延迟一段时间后自动重试。

  介绍一下RabbitMQ的两个特性,一个是Time-To-Live Extensions,另一个是Dead Letter Exchanges。

      Time-To-Live Extensions

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

  Dead Letter Exchange

  被设置TTL的消息在过期后会成为Dead Letter。其实在RabbitMQ中,一共有三种消息的“死亡”形式:

  1. 消息被拒绝。通过调用basic.reject或者basic.nack并且设置的requeue参数为false
  2. 消息因为设置TTL而过期。
  3. 消息进入一条已经达到最大长度的队列

  如果队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新publish到Dead Letter Exchange,通过Dead Letter Exchange路由到其他队列。

  延迟消费

  生产者产生的消息首先会进入缓冲队列(图中红色队列)。通过RabbitMQ提供的TTL扩展,这些消息会被设置过期时间,也就是延迟消费的时间。等消息过期之后,这些消息会通过配置好的DLX转发到实际消费队列(图中蓝色队列),以此达到延迟消费的效果。

  

 

   延迟重试

  消费者发现该消息处理出现异常,比如是因为网络波动引起的异常。那么如果不等待一段时间,直接就重试的话,很可能会导致在这期间内一直无法成功,造成一定的资源浪费。那么可以将其先放在缓冲队列中(图中红色队列),等消息经过一段的延迟时间后再次进入实际消费队列中(图中蓝色队列),此时由于已经过了“较长”的时间,异常的一些波动通常已经恢复,这些消息可以被正常地消费。

  

 

   示例:

  配置队列

  一个延迟队列的实现,需要一个缓冲队列以及一个实际的消费队列。由于在RabbitMQ中,有两种消息过期的配置方式,所以在代码中,共配置了三条队列:

  • delay_queue_per_message_ttl:TTL配置在消息上的缓冲队列。
  • delay_queue_per_queue_ttl:TTL配置在队列上的缓冲队列。
  • delay_process_queue:实际消费队列。

  将delay_queue_per_message_ttl以及delay_queue_per_queue_ttl的DLX配置为同一个,且过期的消息都会通过DLX转发到delay_process_queue。

     配置文件

@Slf4j
@Configuration
public class RabbitConfig {

    /**
     * 发送到该队列的message会在一段时间后过期进入到delay_process_queue
     * 每个message可以控制自己的失效时间
     */
    private final static String DELAY_QUEUE_PER_MESSAGE_TTL_NAME = "delay_queue_per_message_ttl";
    /**
     * 发送到该队列的message会在一段时间后过期进入到delay_process_queue
     * 队列里所有的message都有统一的失效时间
     */
    private final static String DELAY_QUEUE_PER_QUEUE_TTL_NAME = "delay_queue_per_queue_ttl";
    private static int QUEUE_EXPIRATION = 4000;
    /**
     * message失效后进入的队列,也就是实际的消费队列
     */
    private final static String DELAY_PROCESS_QUEUE_NAME = "delay_process_queue";
    /**
     * DLX
     */
    private final static String DELAY_EXCHANGE_NAME = "delay_exchange";
    /**
     * 路由到delay_queue_per_queue_ttl的exchange
     */
    private static String PER_QUEUE_TTL_EXCHANGE_NAME = "per_queue_ttl_exchange";

    /**
     * 创建DLX exchange
     */
    @Bean
    public DirectExchange delayExchange() {
        return new DirectExchange(DELAY_EXCHANGE_NAME);
    }

    /**
     * 创建per_queue_ttl_exchange
     */
    @Bean
    public DirectExchange perQueueTTLExchange() {
        return new DirectExchange(PER_QUEUE_TTL_EXCHANGE_NAME);
    }

    /**
     * 创建delay_queue_per_message_ttl队列
     *
     * @return
     */
    @Bean
    public Queue delayQueuePerMessage() {
        return QueueBuilder.durable(DELAY_QUEUE_PER_MESSAGE_TTL_NAME)
                .withArgument("x-dead-letter-exchange", DELAY_EXCHANGE_NAME)  //DLX,dead letter发送到的exchange
                .withArgument("x-dead-letter-routing-key", DELAY_PROCESS_QUEUE_NAME)  //dead letter携带的routing key
                .build();
    }

    @Bean
    public Queue delayQueuePerQueueTTL() {
        return QueueBuilder.durable(DELAY_QUEUE_PER_QUEUE_TTL_NAME)
                .withArgument("x-dead-letter-exchange", DELAY_EXCHANGE_NAME)
                .withArgument("x-dead-letter-routing-key", DELAY_PROCESS_QUEUE_NAME)
                .withArgument("x-message-ttl", QUEUE_EXPIRATION) //设置队列的过期时间
                .build();
    }

    /**
     * 创建delay_process_queue队列,也就是实际消费队列
     *
     * @return
     */
    @Bean
    public Queue delayProcessQueue() {
        return QueueBuilder.durable(DELAY_PROCESS_QUEUE_NAME).build();
    }

    /**
     * 将DLX绑定到实际消费队列
     *
     * @param delayProcessQueue
     * @param delayExchange
     * @return
     */
    @Bean
    public Binding dlxBinding(Queue delayProcessQueue, DirectExchange delayExchange) {
        return BindingBuilder.bind(delayProcessQueue)
                .to(delayExchange).with(DELAY_PROCESS_QUEUE_NAME);
    }

    @Bean
    public Binding queueTTLBinding(Queue delayQueuePerQueueTTL, DirectExchange perQueueTTLExchange) {
        return BindingBuilder.bind(delayQueuePerQueueTTL).to(perQueueTTLExchange)
                .with(DELAY_QUEUE_PER_QUEUE_TTL_NAME);
    }

    @Bean
    public Binding messageTTLBinding(Queue delayQueuePerMessage, DirectExchange perQueueTTLExchange) {
        return BindingBuilder.bind(delayQueuePerMessage).to(perQueueTTLExchange)
                .with(DELAY_QUEUE_PER_MESSAGE_TTL_NAME);
    }

    /**
     * 定义delay_process_queue队列的Listener Container
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public SimpleMessageListenerContainer processContainer(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
        listenerContainer.setConnectionFactory(connectionFactory);
        listenerContainer.setQueueNames(DELAY_PROCESS_QUEUE_NAME);
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        listenerContainer.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {
            try {
                long deliveryTag = message.getMessageProperties().getDeliveryTag();
                String msg = new String(message.getBody());
                log.info("接收到消息:{}", msg);
                channel.basicAck(deliveryTag, false);
            } catch (Exception e) {
//异常重试 log.info(
"接收消息处理异常:{}重新进入延迟队列", e.getMessage()); channel.basicPublish(PER_QUEUE_TTL_EXCHANGE_NAME, DELAY_QUEUE_PER_QUEUE_TTL_NAME, null, message.getBody()); } }); return listenerContainer; } }

      如何为每个消息设置TTL呢?需要借助MessagePostProcessor。MessagePostProcessor通常用来设置消息的Header以及消息的属性。新建一个ExpirationMessagePostProcessor类来负责设置消息的TTL属性:

public class ExpirationMessagePostProcessor implements MessagePostProcessor {
    private final Long ttl;

    public ExpirationMessagePostProcessor(Long ttl) {
        this.ttl = ttl;
    }

    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        message.getMessageProperties().setExpiration(ttl.toString());
        return message;
    }
}

  向缓冲队列中发送3条消息,过期时间依次为1秒,2秒和3秒。具体的代码如下所示:

  @Test
    public void testDelayQueuePerMessageTTL() throws InterruptedException {
        for (int i = 1; i <= 3; i++) {
            long expiration = i * 1000;
            Object message = "message from delay_queue_per_message_ttl with expiration " + expiration;
            rabbitTemplate.convertAndSend(DELAY_QUEUE_PER_MESSAGE_TTL_NAME, message, new ExpirationMessagePostProcessor(expiration));
        }
        TimeUnit.SECONDS.sleep(10);
    }

  TTL设置在队列上的代码如下:

    @Test
    public void testDelayQueuePerQueueTTL() throws InterruptedException {
        for (int i = 1; i <= 3; i++) {
            Object message = "message from delay_queue_per__ttl with num " + i;
            rabbitTemplate.convertAndSend(DELAY_QUEUE_PER_QUEUE_TTL_NAME, message);
        }
        TimeUnit.SECONDS.sleep(20);
    }

  

  

  

  
posted on 2020-01-12 18:09  溪水静幽  阅读(440)  评论(0)    收藏  举报