你还在用定时任务处理过期数据吗?看我MQ延时队列如何优雅的解决

学习使用,老鸟飞过,欢迎评论交流

为什么要用到延迟队列

在开发项目的时候我们通常会遇到这么一个问题,比如商城项目有一下单逻辑,下单成功数据保存在数据库中,下单成功后需要用户进行支付,如果在30分钟内支付失败,需要修改订单的支付状态为“支付超时”并关闭订单以及回退库存操作,那如何在下单30后准时检查支付结果处理订单状态呢?

你可能想到了一个最简单的方法,就是使用定时任务扫描订单表,判断时间是否支付超时,这样的方式无疑是一种很消耗性能的做法,你试想一下,定时扫描一张数据量很大的表去判断时间和状态,而且99%的扫描都是无效的操作。

那么该如何优雅的解决上述问题呢?我们可以采用延迟队列来实现,Redis和MQ都可以做到,本文章采用RabbitMQ的延迟队列来实现。

延迟队列实现原理

说到延迟队列就要说一说消息的过期时间(存活时间)TTL,RabbitMQ可以给队列设置过期时间,也可以单独给每个消息设置过期时间,如果到了过期时间消息没被消费该消息就会标记为死信消息。

除此之外还有那些消息会成为死信消息?

  • 一是设置了TTL的消息到了TTL过期时间还没被消费,会成为死信
  • 二是消息被消费者拒收,并且reject方法的参数里requeue是false,意味这这个消息不会重回队列,该消息会成为死信,
  • 三是由于队列大小限制,新的消息进来队列可能满了,MQ会淘汰掉最老的消息,这些消息可能会成为死信消息

成为死信的消息会进入一个死信交换机(Dead Letter Exchange)中,死信交换机也是一个普通的交换机而已,根据这一特点,我们可以准备一个队列来接收死信交换机中的死信消息,然后准备一个消费者来消费该队列中的消息,这样一来我们的延迟队列就有思路了,还是按照订单为例流程如下:
在这里插入图片描述

  1. 下单成功(生产者),加入下单消息到队列(order.message)
  2. 队列设置TTL过期时间(10000毫秒),同时指定了死信交换机“delay-exchange”和死信交换机转发消息的队列“delay-message”
  3. 消息进入队列,等待一段时间,如果TTL时间到,订单消息会被MQ扔给死信交换机,死信交换机会把消息扔给指定的死信队列delay-message
  4. 消费者正好监听了死信队列delay-message,就可以获取到消息进行消费,比如检查该消息对应的订单是否支付,做出退库存处理等。

整体效果就是,消息进入order.message队列 延迟 10秒后就 会进入delay-message队列然后被消费者消费处理,这就是一个延迟队列的效果。

注意,这里的delay-exchange死信交换机其实就是一个普通的交换机而已,所以我们可以把上面的两个交换机合并成一个,如下:
在这里插入图片描述

延迟队列实战

第一步,你需要集成RabbitMQ,我这里使用的是SpringBoot集成MQ

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

第二步,对MQ做一些配置

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /

第三步,定义交换机和队列

import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

//rabbitMQ的配置
@Configuration
public class MQConfig {
    //交换机
    public static final String EXCHNAGE_DELAY = "EXCHNAGE_DELAY";
    //订单队列,该队列中的消息设置过期时间
    public static final String QUEUE_ORDER = "QUEUE_ORDER";
    //该队列用来接收死信交换机转发过来的消息
    public static final String QUEUE_DELAY = "QUEUE_DELAY";
    //队列的路由键,该路由键用来接收订单消息传出到订单队列
    public static final String ROUTINGKEY_QUEUE_ORDER = "ROUTINGKEY_QUEUE_ORDER";
    //该路由键用来接收死信交换机转发过来的消息
    public static final String ROUTINGKEY_QUEUE_DELAY = "ROUTINGKEY_QUEUE_DELAY";

    //定义交换机
    @Bean
    public Exchange exchangeDelay(){
        return ExchangeBuilder.topicExchange(EXCHNAGE_DELAY).durable(true).build();
    }
    //该队列中的消息需要设置ttl
    @Bean(QUEUE_ORDER)
    public Queue queueOrder(){
        Map<String,Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange", EXCHNAGE_DELAY);    //过期的消息给哪个交换机的名字
        map.put("x-dead-letter-routing-key", ROUTINGKEY_QUEUE_DELAY);   //死信交换机把消息个哪个个routingkey
        map.put("x-message-ttl", 10000);    //队列过期时间10s
        return new Queue(QUEUE_ORDER,true,false,false,map);
    }
    //该队列接收死信交换机转发过来的消息
    @Bean(QUEUE_DELAY)
    public Queue queueDelay(){
        return new Queue(QUEUE_DELAY,true);
    }
    @Bean
    public Binding queueOrderBinding(){
        return BindingBuilder.bind(queueOrder()).to(exchangeDelay()).with(ROUTINGKEY_QUEUE_ORDER).noargs();
    }
    @Bean
    public Binding queueDelayBinding(){
        return BindingBuilder.bind(queueDelay()).to(exchangeDelay()).with(ROUTINGKEY_QUEUE_DELAY).noargs();
    }
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

第四部,写一个消息发送者

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MQApplication.class)
public class Producer {

    @Autowired
    private RabbitTemplate rabbitTemplate ;

   

    @Test
    public void sendDelayMessage() throws InterruptedException {
        System.out.println("发送消息:我是一个延迟消息,开始时间:"+System.currentTimeMillis());
        rabbitTemplate.convertAndSend(
                MQConfig.EXCHNAGE_DELAY,
                MQConfig.ROUTINGKEY_QUEUE_ORDER,
                "我是一个延迟消息"
        );

        Thread.sleep(20000);
    }
}

第五步,写一个消费者

@Component
public class Consumer {

    @RabbitListener(queues = MQConfig.QUEUE_DELAY)
    public void handler(String message){
        System.out.println("收到消息:"+message+",结束时间:"+System.currentTimeMillis());
    }
}

第六步,测试效果

  • 生产者执行后,观察MQ,QUEUE_ORDER中有消息
    在这里插入图片描述

  • 等待10s之后,消息进入QUEUE_DELAY队列在这里插入图片描述

  • 控制台打印效果

Producer:   发送消息:我是一个延迟消息,开始时间:1606295976347
Consumer: 收到消息:我是一个延迟消息,结束时间:1606295986418

发送消息到收到消息的时间差为 10071 , 忽略网络开销,延迟时间差不多就是我们设置的TTL时间

文章结束,希望对你有所帮助

在实现一个基于消息队列(MQ)的延时队列时,通常需要使用到消息队列提供的延时消息功能。不同的消息队列产品提供了不同的实现方式。以下是使用RabbitMQ实现延时队列的一个简单示例,RabbitMQ通过死信交换机(Dead Letter Exchanges, DLX)和消息的存活时间(time-to-live, TTL)来实现延时队列。 1. 定义一个死信交换机DLX(Dead Letter Exchange)和一个队列DQL Queue,并将队列绑定到DLX上。 2. 发送消息时,设置消息的TTL(存活时间)。如果消息在TTL时间内没有被消费,它会变成死信(Dead Letter)。 3. 死信会被发送到DLX,然后可以根据需要将死信转发到其他队列或者进行处理。 具体步骤如下: 1. 声明一个死信交换机: ```java String dlxName = "dlx"; channel.exchangeDeclare(dlxName, BuiltinExchangeType.DIRECT); ``` 2. 声明一个队列,并设置参数使它成为一个延时队列。队列绑定到死信交换机,并设置消息的TTL为10秒: ```java String queueName = "delayQueue"; Map<String, Object> args = new HashMap<>(); // 设置队列的死信交换机 args.put("x-dead-letter-exchange", dlxName); // 设置消息的存活时间,这里为10秒 args.put("x-message-ttl", 10000); channel.queueDeclare(queueName, true, false, false, args); ``` 3. 发送一条消息,设置消息的TTL为10秒: ```java String message = "延时消息"; AMQP.BasicProperties props = new AMQP.BasicProperties().builder() .expiration("10000") // TTL设置为10秒 .build(); channel.basicPublish("", queueName, props, message.getBytes()); ``` 4. 死信交换机会在消息过期后将消息发送到绑定的队列,此时可以对这个死信进行消费。
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨家巨子@俏如来

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值