延时队列

延时队列能做什么?

  • 订单业务: 在电商/点餐中,都有下单后 30 分钟内没有付款,就自动取消订单。
  • 短信通知: 下单成功后 60s 之后给用户发送短信通知。
  • 失败重试: 业务操作失败后,间隔一定的时间进行失败重试

      对于需要延时操作的事务,不用延时队列可以用定时任务的方式,来查询数据库,但这样的的效率很低。另一种就是用Java中的DelayQueue 位于java.util.concurrent包下,本质是由PriorityQueue和BlockingQueue实现的阻塞优先级队列。这玩意最大的问题就是不支持分布式与持久化。

RabbitMQ 实现思路

RabbitMQ队列本身是没有直接实现支持延迟队列的功能,但可以通过它的Time-To-Live Extensions 与 Dead Letter Exchange 的特性模拟出延迟队列的功能。

消息存活时间

Time-To-Live Extensions(TTL) 即设置消息或队列的存活时间,TTL表明一条消息可以在队列的最大存活时间,当一条消息设置了最大存活时间或者进入了设置了TTL的队列,这条消息会在TTL 后死亡,就成了 dead letter,。如果既设置了消息也设置了队列,以最小的时间为准。

死信交换机

上面的消息如果设置了 dead letter exachange(DLX)死信交换机,在消息死亡后,会进到死信交换机,然后路由到其他队列,继续消费,实现队列延时。


设置 TTL
设置 key

>路由



根据key路由


ACK

生产者生产消息

交换机

队列

没有消费,变成死信

死信交换机

死信交换机

其他队列

消费者消费

从队列中删除


具体编码

定义队列

public static final String QUEUE_1 = "DQUEUE_1";
    public static final String EXCHANGE_1= "EXCHANGE_1";
    public static final String ROUTING_KEY_1 = "ROUTING_KEY_1";

    public static final String QUEUE_2 = "QUEUE_2"; //延时队列TTL名称:DELAY_QUEUE_A
    public static final String EXCHANGE_2 = "EXCHANGE_2"; //DLX 死信交换机,消息到期发到这个
    public static final String ROUTING_KEY_2 = "ROUTING_KEY_2";//路由关键字


    @Bean
    public Queue Queue1(){
        return new Queue(QUEUE_1,true);
    }
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(EXCHANGE_1);
    }
    @Bean
    public Binding rBinding(){
        return BindingBuilder.bind(Queue1()).to(topicExchange()).with(ROUTING_KEY_1);
    }
    /**
     * 延迟队列配置
     * <p>
     * 1、params.put("x-message-ttl", 5 * 1000);
     * TODO 第一种方式是直接设置 Queue 延迟时间 但如果直接给队列设置过期时间,这种做法不是很灵活,(当然二者是兼容的,默认是时间小的优先)
     * 2、rabbitTemplate.convertAndSend(book, message -> {
     * message.getMessageProperties().setExpiration(2 * 1000 + "");
     * return message;
     * });
     * TODO 第二种就是每次发送消息动态设置延迟时间,这样我们可以灵活控制
     **/
    @Bean
    public Queue delayProcessQueue() {
        Map<String, Object> params = new HashMap<>();
        // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
        params.put("x-dead-letter-exchange", EXCHANGE_1);
        // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
        params.put("x-dead-letter-routing-key", ROUTING_KEY_1);
        return new Queue(QUEUE_2, true, false, false, params);
    }

    /**
     * 死信交换机
     * @return
     */
    @Bean
    public DirectExchange delayExchange() {
        return  new DirectExchange(EXCHANGE_2);
    }
    @Bean
    public Binding dlxBinding(){
        return BindingBuilder.bind(delayProcessQueue()).to(delayExchange()).with(ROUTING_KEY_2);
    }

控制器

@ApiOperation(value = "rabbitmq延时队列")
    @RequestMapping(value = "/proDelayMsg",method = {RequestMethod.GET})
    @ResponseBody
    public String proDelayMsg() {
        Student stu = new Student();
        stu.setId((short)1);
        stu.setName("学习延时队列的旭东");
        stu.setAddress("上海宝山");
        log.info("生产者生产  延时  消息:"+stu);
        this.rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_2,RabbitConfig.ROUTING_KEY_2 ,stu,message ->{
            // TODO 第一句是可要可不要,根据自己需要自行处理
            message.getMessageProperties().setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, Student.class.getName());
            // TODO 如果配置了 params.put("x-message-ttl", 5 * 1000); 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间
            message.getMessageProperties().setExpiration(5 * 1000 + "");
            return message;
        } );
        log.info("[发送时间] - [{}]", LocalDateTime.now());
        return "success";
    }

消费者监听

@RabbitListener(queues = {RabbitConfig.QUEUE_1})
    public void listenerDelayAck(Student stu, Message message, Channel channel) {
        log.info("[listenerDelayQueue 监听的消息] - [消费时间] - [{}] - [{}]", LocalDateTime.now(), stu.toString());
        try {
            // TODO 通知 MQ 消息已被成功消费,可以ACK了
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (IOException e) {
            // TODO 如果报错了,那么我们可以进行容错处理,比如转移当前消息进入其它队列
            try {
                channel.basicRecover();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

测试结果

[2019-05-06 14:29:36:259] [INFO] - com.example.controller.StudentRabbitMqController.proDelayMsg(StudentRabbitMqController.java:60) - 生产者生产  延时  消息:[id:1,age:null,name:学习延时队列的旭东,address:上海宝山]
[2019-05-06 14:29:36:727] [INFO] - com.example.controller.StudentRabbitMqController.proDelayMsg(StudentRabbitMqController.java:68) - [发送时间] - [2019-05-06T14:29:36.726]
[2019-05-06 14:29:42:139] [INFO] - com.example.handler.BookHandler.listenerDelayAck(BookHandler.java:69) - [listenerDelayQueue 监听的消息] - [消费时间] - [2019-05-06T14:29:42.139] - [[id:1,age:null,name:学习延时队列的旭东,address:上海宝山]]

部分配置内容在上一篇