RabbitMQ(五)延时任务

RabbitMQ(五)延时任务

延时队列

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

场景一:在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将进行一场处理。这是就可以使用延时队列将订单信息发送到延时队列。

场景二:用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将用户指令发送到延时队列,当指令设定的时间到了再将指令推送到只能设备。

场景

延迟队列多用于需要延迟工作的场景。最常见的是以下两种场景:

1、延迟消费。

比如:

  1. 用户生成订单之后,需要过一段时间校验订单的支付状态,如果订单仍未支付则需要及时地关闭订单。
  2. 用户注册成功之后,需要过一段时间比如一周后校验用户的使用情况,如果发现用户活跃度较低,则发送邮件或者短信来提醒用户使用。

2、延迟重试。

比如消费者从队列里消费消息时失败了,但是想要延迟一段时间后自动重试。

如果不使用延迟队列,那么我们只能通过一个轮询扫描程序去完成。这种方案既不优雅,也不方便做成统一的服务便于开发人员使用。但是使用延迟队列的话,我们就可以轻而易举地完成。

实现

Rabbitmq实现延时队列一般而言有两种形式:

  • 第一种方式:利用两个特性: Time To Live(TTL)Dead Letter Exchanges(DLX)
  • 第二种方式:利用rabbitmq中的插件x-delay-message

方式一:Dead Letter

实现思路

在介绍具体的实现思路之前,我们先来介绍一下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的Queue可以配置x-dead-letter-exchangex-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。

  • x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange。

  • x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送。

流程图

聪明的你肯定已经想到了,如何将RabbitMQ的TTL和DLX特性结合在一起,实现一个延迟队列。

针对于上述的延迟队列的两个场景,我们分别有以下两种流程图:

延迟消费

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

img

延迟重试

延迟重试本质上也是延迟消费的一种,但是这种模式的结构与普通的延迟消费的流程图较为不同,所以单独拎出来介绍。

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

img

代码实现

接下来我们将介绍如何在Spring Boot中实现基于RabbitMQ的延迟队列。我们假设读者已经拥有了Spring Boot与RabbitMQ的基本知识。

初始化工程

首先我们在Intellij中创建一个Spring Boot工程,并且添加spring-boot-starter-amqp扩展。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--fast json依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.40</version>
        </dependency>
配置application.yml
server:
  port: 8088

spring:
  application:
    name: rabbitmq_delay
  rabbitmq:
    host: 192.168.3.253
    port: 5672
    username: admin
    password: 123456
    publisher-confirms: true
    listener:
      simple:
        concurrency: 10 #并发消费者的初始化值
        max-concurrency: 20 #并发消费者的最大值
        prefetch: 5 #每个消费者每次监听时可拉取处理的消息数量。

配置队列

添加一个QueueEnum队列枚举配置,该枚举内配置队列的ExchangeQueueNameRouteKey等相关内容,如下所示:

package com.lay.rabbitmqdelay.config;

/**
 * @Description:消息队列枚举配置
 * @Author: lay
 * @Date: Created in 19:31 2018/12/20
 * @Modified By:IntelliJ IDEA
 */
public enum QueueEnum {
    //延迟消息通知队列
    MESSAGE_QUEUE("message.center.direct", "message.create", "message_center.create"),

    //消息通知ttl队列
    MESSAGE_TTL_QUEUE("message.center.topic.ttl", "message.create.ttl", "message_center.create.ttl");

    //交换机名称
    private String exchange;
    //队列名称
    private String name;
    //路由键
    private String routingKey;

    QueueEnum(String exchange, String name, String routingKey) {
        this.exchange = exchange;
        this.name = name;
        this.routingKey = routingKey;
    }
    public String getExchange() {
        return exchange;
    }


    public String getName() {
        return name;
    }

    public String getRoutingKey() {
        return routingKey;
    }


}

可以看到MESSAGE_QUEUE队列配置跟我们之前章节的配置一样,而我们另外新创建了一个后缀为ttl的消息队列配置。我们采用的这种方式是RabbitMQ消息队列其中一种的延迟消费模块,通过配置队列消息过期后转发的形式。

这种模式比较简单,我们需要将消息先发送到ttl延迟队列内,当消息到达过期时间后会自动转发到ttl队列内配置的转发Exchange以及RouteKey绑定的队列内完成消息消费。

下面我们来模拟消息通知的延迟消费场景,先来创建一个名为MessageRabbitMqConfiguration的队列配置类,该配置类内添加消息通知队列配置以及消息通过延迟队列配置,如下所示:

package com.lay.rabbitmqdelay.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description: 消息队列配置信息
 * @Author: lay
 * @Date: Created in 19:40 2018/12/20
 * @Modified By:IntelliJ IDEA
 */
@Configuration
public class RabbitMqConfig {

    //消息中心实际消费队列交换配置
    @Bean
    DirectExchange messageDirectExchange() {
        return (DirectExchange) ExchangeBuilder
                .directExchange(QueueEnum.MESSAGE_QUEUE.getExchange())
                .durable(true)
                .build();
    }

    //消息中心延迟消费交换配置
    @Bean
    DirectExchange messageDirectTtlExchange() {
        return (DirectExchange) ExchangeBuilder
                .directExchange(QueueEnum.MESSAGE_TTL_QUEUE.getExchange())
                .durable(true)
                .build();
    }

    //消息中心实际消费队列配置
    @Bean
    Queue messageQueue() {
        return new Queue(QueueEnum.MESSAGE_QUEUE.getName());
    }
    //消息中心Ttl队列配置
    @Bean
    Queue messageTtlQueue() {
        return QueueBuilder
                .durable(QueueEnum.MESSAGE_TTL_QUEUE.getName())
                .withArgument("x-dead-letter-exchange", QueueEnum.MESSAGE_QUEUE.getExchange())
                .withArgument("x-dead-letter-routing-key", QueueEnum.MESSAGE_QUEUE.getRoutingKey())
                .build();
    }

    //消息中心实际消息交换机与队列绑定
    @Bean
    Binding messageBinding(DirectExchange messageDirectExchange, Queue messageQueue) {
        return BindingBuilder
                .bind(messageQueue)
                .to(messageDirectExchange)
                .with(QueueEnum.MESSAGE_QUEUE.getRoutingKey());

    }

    //消息中心TTL绑定实际TTL绑定延迟消费交换机与队列
    @Bean
    Binding messageTtlBinding(DirectExchange messageDirectTtlExchange,Queue messageTtlQueue){
        return BindingBuilder
                .bind(messageTtlQueue)
                .to(messageDirectTtlExchange)
                .with(QueueEnum.MESSAGE_TTL_QUEUE.getRoutingKey());
    }
}

我们声明了消息通知队列的相关ExchangeQueueBinding等配置,将message.center.create队列通过路由键message.center.create绑定到了message.center.direct交换上。

除此之外,我们还添加了消息通知延迟队列ExchangeQueueBinding等配置,将message.center.create.ttl队列通过message.center.create.ttl路由键绑定到了message.center.topic.ttl交换上。

我们仔细来看看messageTtlQueue延迟队列的配置,跟messageQueue队列配置不同的地方这里多出了x-dead-letter-exchangex-dead-letter-routing-key两个参数,而这两个参数就是配置延迟队列过期后转发的ExchangeRouteKey.。

其中,x-dead-letter-exchange声明了队列里的死信转发到的DLX名称。x-dead-letter-routing-key声明了这些死信在转发时携带的routing-key名称。

只要在创建队列时对应添加了这两个参数,在RabbitMQ管理平台看到的队列配置就不仅是单纯的Direct类型的队列类型,如下图所示:

img

队列类型差异

在上图内我们可以看到message.center.create.ttl队列多出了DLXDLK的配置,这就是RabbitMQ死信交换的标志。
满足死信交换的条件,在官方文档中表示:

Messages from a queue can be ‘dead-lettered’; that is, republished to another exchange when any of the following events occur:

The message is rejected (basic.reject or basic.nack) with requeue=false,
The TTL for the message expires; or
The queue length limit is exceeded.

  • 该消息被拒绝(basic.reject或 basic.nack),requeue = false
  • 消息的TTL过期
  • 队列长度限制已超出
    官方文档地址

我们需要满足上面的其中一种方式就可以了,我们采用满足第二个条件,采用过期的方式。

生产者

MessageProvider.java

package com.lay.rabbitmqdelay.provider;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 10:09 2018/12/21
 * @Modified By:IntelliJ IDEA
 */
@Component
public class MessageProvider {
    private static final Logger log= LoggerFactory.getLogger(MessageProvider.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //发送延迟消息
    public void sendMessage(Object messageContent,String exchange,String routingKey,final long delayTimes){
        if(!StringUtils.isEmpty(exchange)){
            log.info("延迟: {}毫秒写入消息队列:{},消息内容:{}",delayTimes,routingKey, JSON.toJSONString(messageContent));
            //执行发送消息到指定队列
            rabbitTemplate.convertAndSend(exchange,routingKey,messageContent,message -> {
                //设置延迟毫秒值
                message.getMessageProperties().setExpiration(String.valueOf(delayTimes));
                return message;
            });
        }else{
            log.error("未找到队列消息:{},所属的交换机",exchange);
        }
    }
}

由于我们在 pom.xml配置文件内添加了RabbitMQ相关的依赖并且在上面application.yml文件内添加了对应的配置,SpringBoot为我们自动实例化了RabbitTemplate,该实例可以发送任何类型的消息到指定队列。
我们采用convertAndSend方法,将消息内容发送到指定ExchangeRouterKey队列,并且通过setExpiration方法设置过期时间,单位:毫秒。

消费者

接下来创建一个名为MessageConsumer的消费者类,该类需要监听消息通知队列,代码如下所示:

package com.lay.rabbitmqdelay.consumer;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 10:32 2018/12/21
 * @Modified By:IntelliJ IDEA
 */
@Component
@RabbitListener(queues = "message.create")
public class MessageConsumer {
    private static final Logger log= LoggerFactory.getLogger(MessageConsumer.class);
    @RabbitHandler
    public void process(@Payload String content, Message message){
        log.info("消费内容:{}",content);
        log.info("消费时间:{}",new Date());
        log.info("Message内容: {}", JSON.toJSONString(message));
    }
}

@RabbitListener注解内配置了监听的队列,这里配置内容是QueueEnum枚举内的queueName属性值,当然如果你采用常量的方式在注解属性上是直接可以使用的,枚举不支持这种配置,这里只能把QueueName字符串配置到queues属性上了。
由于我们在消息发送时采用字符串的形式发送消息内容,这里在@RabbitHandler处理方法的参数内要保持数据类型一致!

测试

我们在test目录下创建一个测试类,如下所示:



    package com.lay.rabbitmqdelay;
    
    /**
     * @Description:
     * @Author: lay
     * @Date: Created in 10:26 2018/12/21
     * @Modified By:IntelliJ IDEA
     */
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = RabbitMqDelayApplication.class)
    public class RabbitMqDelayApplicationTesst {
    
        @Autowired
        private MessageProvider messageProvider;
    
        @Test
        public void testDelay(){
            messageProvider.sendMessage("测试延迟消费信息,写入时间: "+new Date(),
                    QueueEnum.MESSAGE_TTL_QUEUE.getExchange(),
                    QueueEnum.MESSAGE_TTL_QUEUE.getRoutingKey(),
                    10000);
        }
    }
问题

第一种方式:经过测试,我们可以发现,当我们先增加一条过期时间大(10000)的A消息进入,之后再增加一个过期时间小的(1000)消息B,并没有出现想象中的B消息先被消费,A消息后被消费,而是出现了当10000过去的时候,AB消息同时被消费,也就是B消息的消费被阻塞了。

为什么会出现这样的现象呢?
我们知道利用TTL DLX特性实现的方式,实际上在第一个延时队列C里面设置了dlx,生产者生产了一条带ttl的消息放入了延时队列C中,等到延时时间到了,延时队列C中的消息变成了死信,根据延时队列C中设置的dlx的exchange的转发规则,转发到了实际消费队列D中,当该队列中的监听器监听到消息时就会正式开始消费。那么实际上延时队列中的消息也是放入队列中的,队列满足先进先出,而延时大的消息A还没出队,所以B消息也不能顺利出队。

解决办法:利用rabbitMQ插件x-delay-message

方式二:x-delay-message

为了解决上面的问题,Rabbitmq实现了一个插件x-delay-message来实现延时队列。

安装

选择rabbitmq_delayed_message_exchange插件,选择3.6版本,进行下载

#将安装包进行解压
uzip rabbitmq_delayed_message_exchange-20171215-3.6.x.zip

#将插件移到rabbitmq安装的路径
cp -r rabbitmq_delayed_message_exchange-20171215-3.6.x.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/plugins

#启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
配置队列

XdelayConfig.java

package com.lay.rabbitmqdelay.config;

/**
 * @Description: 利用x-delayed-message-exchange插件,解决rabbitmq死信队列延时大会阻塞延时小的消息
 * @Author: lay
 * @Date: Created in 13:16 2018/12/21
 * @Modified By:IntelliJ IDEA
 */
@Configuration
public class XdelayConfig {
    //队列名
    public static final String X_DELAY_MESSAGE_QUEUE="x.delay.message";
    public static final String X_DELAY_MESSAGE_EXCHANGE="x.delay.exchange";
    public static final String X_DELAY_ROUTING_KEY="x.delay.routing.key";

    //创建一个真实消费队列
    @Bean
    Queue xDelayMessageQueue(){
        return new Queue(X_DELAY_MESSAGE_QUEUE,true);
    }

    @Bean
    CustomExchange xDelayExchange(){
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(X_DELAY_MESSAGE_EXCHANGE, "x-delayed-message", true, false, args);
    }

    @Bean
    Binding xDelayMessageBinding(Queue xDelayMessageQueue,CustomExchange xDelayExchange){
        return BindingBuilder
                .bind(xDelayMessageQueue)
                .to(xDelayExchange)
                .with(X_DELAY_ROUTING_KEY)
                .noargs();
    }
}

生产者

XdelayProvider.java

package com.lay.rabbitmqdelay.provider;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 13:32 2018/12/21
 * @Modified By:IntelliJ IDEA
 */
@Component
public class XdelayProvider {

    private static final Logger log= LoggerFactory.getLogger(XdelayProvider.class);
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendXDelayMessage(Object messageContent,String exchange,String routingKey,final long delayTimes){
        if(!StringUtils.isEmpty(exchange)){
            log.info("延迟: {} 毫秒写入x-delay消息队列:{},消息内容:{}",delayTimes,routingKey, JSON.toJSONString(messageContent));
            //执行发送消息到指定队列
            rabbitTemplate.convertAndSend(exchange,routingKey,messageContent,message -> {
                //设置延迟毫秒值
                message.getMessageProperties().setDelay((int) delayTimes);
                return message;
            });
        }else{
            log.error("未找到队列消息:{},所属的交换机",exchange);
        }
    }
}

消费者

XDelayConsumer.java

package com.lay.rabbitmqdelay.consumer;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 10:32 2018/12/21
 * @Modified By:IntelliJ IDEA
 */
@Component
@RabbitListener(queues = XdelayConfig.X_DELAY_MESSAGE_QUEUE)
public class XDelayConsumer {
    private static final Logger log= LoggerFactory.getLogger(XDelayConsumer.class);
    @RabbitHandler
    public void process(@Payload String content, Message message){
        log.info("x-delay消费内容:{}",content);
        log.info("x-delay消费时间:{}",new Date());
        log.info("Message内容: {}", JSON.toJSONString(message));

    }
}

测试类
package com.lay.rabbitmqdelay;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 10:26 2018/12/21
 * @Modified By:IntelliJ IDEA
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMqDelayApplication.class)
public class RabbitMqXDelayApplicationTesst {

    @Autowired
    private XdelayProvider xdelayProvider;

    @Test
    public void testDelay(){
        xdelayProvider.sendXDelayMessage("测试x-delay延迟消费信息,写入时间: "+new Date(),
                XdelayConfig.X_DELAY_MESSAGE_EXCHANGE,
                XdelayConfig.X_DELAY_ROUTING_KEY,
                10000);
    }
}

github源码

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
延时队列是 RabbitMQ 中的一个常见概念,它允许消息在一段延时后才会被消费者消费。这在某些场景下非常有用,例如需要在未来的某个特定时间才执行某个任务。 在 RabbitMQ 中实现延时队列有几种方法,其中最常用的方法是使用插件 `rabbitmq_delayed_message_exchange`。这个插件允许你创建一个特殊的交换机,它支持对消息设置延时时间,然后将消息路由到延迟队列中。 具体实现步骤如下: 1. 确保你已经安装了 `rabbitmq_delayed_message_exchange` 插件。如果没有安装,可以通过运行 `rabbitmq-plugins enable rabbitmq_delayed_message_exchange` 命令来安装。 2. 创建一个类型为 `x-delayed-message` 的交换机,并将其配置为使用 `rabbitmq_delayed_message_exchange` 插件。你可以使用 RabbitMQ 的管理界面或者命令行工具来创建交换机。 3. 创建一个普通的队列,并将其绑定到延迟交换机上。 4. 在生产者发送消息时,设置消息的 `x-delay` 头部属性来指定延迟时间,单位是毫秒。消息会被发送到延迟交换机,并根据延迟时间被路由到延迟队列中。 5. 消费者从延迟队列中消费消息,延迟时间到达后才能获取到消息。 需要注意的是,延时队列并不是 RabbitMQ 的原生特性,而是通过插件来实现的。因此,在使用延时队列之前,你需要确保 RabbitMQ 已经安装了相应的插件。 希望这个回答对你有帮助。如果你还有其他问题,请随时提问!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值