RabbitMQ——延时队列

延时队列

概念

延时队列其实是死信队列的中消息超时的一种演变,当普通队列没有消费者时,设置了过期时间的消息都会转发到死信队列,交由死信队列的消费者操作,而当满足以上情形的死信队列,我们称之为延时队列。

具体的应用场景

  • 订单在十分钟之内未支付则自动取消
  • 新创建的店铺,如果十天内没有上传过商品,则自动发送消息提醒
  • 用户注册成功后,如果三天内没有登录则进行短信提醒

这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务

本文贯彻的例子

如下图所示,生产者发送一条消息通过交换机分别给QA、QB队列,设置过期时间,当过期时间到达时,转发消息给死信交换机,再由死信队列发送给消费者。

在这里插入图片描述

完成基本功能

使用springboot框架快速搭建一个web项目

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<!--RabbitMQ 依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit-test</artifactId>
    <scope>test</scope>
</dependency>

配置环境

spring:
  rabbitmq:
    host: 121.40.45.37
    port: 5672
    username: admin
    password: Hkx123

添加swagger配置类

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket webApiConfig(){
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApiInfo())
                .select()
                .build();
    }
    private ApiInfo webApiInfo(){
        return new ApiInfoBuilder()
                .title("rabbitmq 接口文档")
                .description("本文档描述了 rabbitmq 微服务接口定义")
                .version("1.0")
                .contact(new Contact("yellowstar", "http://yellowstar.top","614028802@qq.com"))
                .build();
    }
}

添加交换机与队列的配置类

在之前的学习过程中,我们都将交换机与队列的配置过程放在消费者代码中,而在springboot中,我们需要新建一个配置类,将交换机与队列的配置放在其中。

@Configuration
public class TtlQueueConfig {
    //设置普通交换机
    public static String X_EXCHANGE = "X";
    //设置死信交换机
    public static String Y_DEAD_LETTER_EXCHANGE = "Y";
    //设置两个普通队列
    public static String QUEUE_A = "QA";
    public static String QUEUE_B = "QB";
    //设置死信队列
    public static String DEAD_LETTER_QUEUE = "QD";

    //声明普通交换机
    @Bean("Xexchange")
    public DirectExchange Xexchange(){
        return new DirectExchange(X_EXCHANGE);
    }
    //声明死信交换机
    @Bean("Yexchange")
    public DirectExchange Yexchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    //声明普通队列A,过期时间10s
    @Bean("queueA")
    public Queue queueA(){
        HashMap<String, Object> argument = new HashMap<>(3);
        //设置死信交换机
        argument.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信routingKey
        argument.put("x-dead-letter-routing-key","YD");
        //声明队列过期时间,党委为毫秒
        argument.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(argument).build();
    }

    //声明普通队列A绑定普通交换机
    @Bean
    public Binding queueAbindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("Xexchange") DirectExchange Xexchange){
        return BindingBuilder.bind(queueA).to(Xexchange).with("XA");
    }

    //声明普通队列B,过期时间30s
    @Bean("queueB")
    public Queue queueB(){
        HashMap<String, Object> argument = new HashMap<>(3);
        //设置死信交换机
        argument.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信routingKey
        argument.put("x-dead-letter-routing-key","YD");
        //声明队列过期时间,党委为毫秒
        argument.put("x-message-ttl",30000);
        return QueueBuilder.durable(QUEUE_B).withArguments(argument).build();
    }

    //声明普通队列B绑定普通交换机
    @Bean
    public Binding queueBbindingX(@Qualifier("queueB") Queue queueB,
                                  @Qualifier("Xexchange") DirectExchange Xexchange){
        return BindingBuilder.bind(queueB).to(Xexchange).with("XB");
    }

    //声明死信队列
    @Bean("queueD")
    public Queue queueD(){
        return new Queue(DEAD_LETTER_QUEUE);
    }

    //声明死信队列绑定死信交换机
    @Bean
    public Binding queueDbindingY(@Qualifier("queueD") Queue queueD,
                                  @Qualifier("Yexchange") DirectExchange Yexchange){
        return BindingBuilder.bind(queueD).to(Yexchange).with("YD");
    }
}

编写生产者代码

我们要求访问 http://localhost:8080/ttl/sendMsg/xxx,最后的xxx为我们需要发送的信息

@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message){
        log.info("当前时间:{},生产者发送消息:{}",new Date().toString(),message);
        //像队列A和队列B分别发送消息
        rabbitTemplate.convertAndSend("X","XA","消息来源10s的队列A:" + message);
        rabbitTemplate.convertAndSend("X","XB","消息来源30s的队列B:" + message);
    }
}

编写消费者代码

@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);
    }
}

测试

启动服务,访问 http://localhost:8080/ttl/sendMsg/helloRabbitMQ ,查看控制台输出

可以看到控制台输出的间隔时间为10s和30s,这说明我们的案例成功了

在这里插入图片描述

延时队列优化

上述例子虽然实现了延时队列的效果,但是有那么一个问题,我们设置了两个队列10秒和30秒,当我们的需求新增一个60秒的队列怎么办?手动在增加一个吗?如果新增100个队列呢?所以我们就在想,能不能在配置文件中不设置过期时间,交给客户端来设置。以下QC队列就是我们需要实现的

在这里插入图片描述

更新配置文件

新增以下内容

//新增普通队列
public static String QUEUE_C = "QC";
//声明普通队列C
@Bean("queueC")
public Queue queueC(){
    HashMap<String, Object> argument = new HashMap<>(3);
    //设置死信交换机
    argument.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
    //设置死信routingKey
    argument.put("x-dead-letter-routing-key","YD");
    return QueueBuilder.durable(QUEUE_C).withArguments(argument).build();
}

更新生产者代码

新增以下内容

@GetMapping("/sendTTLMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime){
    log.info("当前时间:{},生产者发送消息:{},消息延迟时间为:{}",new Date().toString(),message,ttlTime);
    rabbitTemplate.convertAndSend("X","QC","消息来源队列C:" + message,correlationData -> {
        //设置过期时间
        correlationData.getMessageProperties().setExpiration(ttlTime);
        return correlationData;
    });
}

测试

发送以下两个请求测试

  • http://localhost:8080/ttl/sendTTLMsg/hello1/20000
  • http://localhost:8080/ttl/sendTTLMsg/hello2/2000

我们发现是可以以进行通过一个队列发送两条不同的延时消息的

在这里插入图片描述

基于死信队列延时消息的问题

通过上面例子我们可以看到,虽然发送并接收到了两条延时消息,但是延时2秒的消息是在延时20秒的消息之后接收到的,这其实一个比较大的bug,因为队列是有序的,先进先出,所以即使你超时时间设置的再快,也得等前面的消息被处理了才能轮得到你。

这个问题需要使用插件才能解决

基于插件解决延时消息

安装插件

小黄的rabbitmq是部署在服务器上使用docker运行的,安装插件的过程可以参考 [Docker安装Rabbitmq及其延时队列插件]

安装完成之后,在web端我们可以看到延时消息类型的交换机

在这里插入图片描述

原理

基于死信队列的延时消息,我们是在消息进入队列的时候,设置消息的过期时间,而基于插件的延时消息,交换机本身就有一个延时的功能,等到时间到了在交给队列发送给消费者

在这里插入图片描述

编写配置类

@Configuration
public class DelayedQueueConfig {
    //声明延时交换机名称
    public static String DELAYED_EXCHANGE = "delayed_exchange";
    //声明延时routingkey
    public static String DELAYED_ROUTINGKEY = "delayed_routing_key";
    //声明延时队列名称
    public static String DELAYED_QUEUE = "delayed_queue";

    //声明队列
    @Bean
    public Queue delayedQueue(){
        return new Queue(DELAYED_QUEUE);
    }

    //声明交换机
    @Bean
    public CustomExchange delayedExchange(){
        HashMap<String, Object> arguments = new HashMap<>();
        //设置自定义交换机类型
        arguments.put("x-delayed-type","direct");
        return new CustomExchange(DELAYED_EXCHANGE,"x-delayed-message",true,false,arguments);
    }

    //绑定交换机和队列
    @Bean
    public Binding delayedQueueBindingDelayedExchange(
            @Qualifier("delayedQueue") Queue delayedQueue,
            @Qualifier("delayedExchange") CustomExchange delayedExchange
    ){
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTINGKEY).noargs();
    }
}

编写生产者

@GetMapping("/sendDelayedMsg/{message}/{delayedTime}")
public void sendDelayedMsg(@PathVariable String message,@PathVariable Integer delayedTime){
    log.info("当前时间:{},生产者发送消息:{},消息延迟时间为:{}",new Date().toString(),message,delayedTime);
    rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE,DelayedQueueConfig.DELAYED_ROUTINGKEY, message
                                  ,correlationData -> {
                                      correlationData.getMessageProperties().setDelay(delayedTime);
                                      return correlationData;
                                  });
}

编写消费者

@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);
    }
}

测试

我们还是先发一个延时20s的内容,再发一个延时2s的内容

  • http://localhost:8080/ttl/sendDelayedMsg/hello1/20000
  • http://localhost:8080/ttl/sendDelayedMsg/hello2/2000

观察控制台输出,可以发现与我们的预期效果一样,完美!!!

在这里插入图片描述

### RabbitMQ中配置延时队列以确保高优先级消息或最新消息被优先处理 为了确保在RabbitMQ中配置的延时队列能够使高优先级的消息或是最新的消息得到优先处理,需要综合考虑多个方面。一方面,在创建队列的时候要指定`x-max-priority`参数来定义最大优先级数值,从而支持基于优先级的消息调度机制[^1];另一方面,对于延时功能,则需借助于特定类型的交换机——即带有`x-delayed-message`特性的自定义插件实现方式。 具体来说: - **启用必要的插件**:首先应当确认已安装并启用了`rabbitmq_delayed_message_exchange`插件,这一步骤是构建具备延时能力的基础环境所必需的操作[^5]。 - **声明带优先级的支持队列**:当声明用于承载待发送数据包的目标队列时,应指明其具有多等级别的特性,并设定合理的最高级别界限值(例如8)。这样做的目的是为了让后续进入该通道内的每一条记录都能够携带自身的紧急程度标识符,进而影响到它们之间相对顺序安排的可能性。 ```yaml spring.rabbitmq.listener.direct.concurrency=3 spring.rabbitmq.template.exchange=my.priority.delayed.exchange spring.rabbitmq.queue.name=priority.delayed.queue spring.rabbitmq.queue.arguments.x-max-priority=8 ``` - **配置延迟交换器**:接着就是针对上述提到过的特殊性质交换实例进行初始化工作了。这里会涉及到一些额外选项的选择与调整,比如将类型设为`'x-delayed-message'`以及关联起之前建立好的持久化存储空间实体等操作。 ```java @Bean public CustomExchange delayedExchange() { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); return new CustomExchange("my.priority.delayed.exchange", "x-delayed-message", true, false, args); } ``` - **发布含有时间戳和优先权级别的消息体**:最后一点也是最为重要的环节在于实际向网络上传输的信息内容本身的设计上。除了常规的有效载荷之外,还需附加两个关键属性字段:“expiration”用来表示预期等待周期长度,“priority”则决定了当前项在整个序列里边的位置权重关系[^2]。 ```java MessageProperties messageProperties = new MessageProperties(); messageProperties.setExpiration(String.valueOf(5000)); // 设置过期时间为5秒 messageProperties.setPriority(8); // 设定较高优先级 amqpTemplate.convertAndSend(exchangeName, routingKey, content, messageProperties); ``` 通过以上措施可以在很大程度上满足关于如何让某些重要指令尽快被执行的需求目标,同时也兼顾到了不同场景下灵活应对变化的能力需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值