订单超时取消之延迟队列

什么是延迟队列?

我们先来看一个场景:以淘宝购物为例,当你提交订单之后有30分钟的支付时间,假如你30分钟之后还没有进行支付,订单就会被取消。现在让你来实现这个功能,你准备如何实现?

相信很多小伙伴第一反应就是定时轮询,设定一个定时任务去扫订单数据,一旦发现超过30分钟未支付的订单,就将订单状态update成已取消,这是一种最简单的方法,也是最容易实现的。这种方案的弊端在于:当数据量小时,不会存在问题,当数据量越来越大时,定时扫表会变得越来越慢,而且频繁的扫表会影响下单的效率

延迟队列就是用来解决这一类问题的,那么什么是延迟队列呢?

延迟队列是为了解决任务推迟执行的问题,消息进入延迟队列之后暂时不能被消费,等超过了设定的时间才能被消费者进行消费

可以想像一下这样一种场景,每个任务进入队列的时候都打上了一个时间标签,任务1(10分钟后执行)、任务2(30分钟后执行)、任务3(60分钟后执行),当到了标签对应的时间之后,任务才能被执行

常见的可以使用延迟队列场景:

  • 淘宝下单后,30分钟未支付要取消订单
  • 外卖订单1分钟后,短信提醒客户
  • 3天未评论自动好评

总之,需要延后执行的任务都可以用延迟队列来解决


延迟队列的实现方法

1、DelayQueue

在JDK的java.util.concurrent包中提供了延迟队列的实现DelayQueue,它提供了在指定的时间才能获取队列中元素的功能,队列头的元素是最接近过期时间的元素。如果没有过期元素,使用poll()方法会返回null。下面看代码实现

public class DelayTask implements Delayed {

    private String msg;
    private long executeTime;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public long getExecuteTime() {
        return executeTime;
    }

    public void setExecuteTime(long executeTime) {
        this.executeTime = executeTime;
    }

    public DelayTask(long delayTime, String msg){
        //到期时间 = 当前时间 + 延迟时间
        this.executeTime = System.currentTimeMillis() + delayTime;
        this.msg = msg;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.executeTime - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        return (int)(this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }
}

DelayQueue中的元素需要实现Delayed接口,我们定义一个任务来实现Delayed接口,主要有两个方法

  • getDelay(TimeUnit unit) 定义了剩余到期时间
  • compareTo(Delayed o) 定义了元素的排序规则
public class DelayQueueTest {
    public static void main(String[] args){
        //创建一个延迟队列
        DelayQueue<DelayTask> delayQueue = new DelayQueue<DelayTask>();
        DelayQueueTest delayQueueTest = new DelayQueueTest();
        //生产者放入消息
        delayQueueTest.producer(delayQueue);
        //消费者消费消息
        delayQueueTest.consumer(delayQueue);

    }

    //定义一个消费者,启动一个线程,循环从队列中拿元素
    private void consumer(DelayQueue<DelayTask> delayTasks){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        DelayTask delayTask = delayTasks.take();
                        System.out.println("消息消费时间:" + getCurrentTime() + ",msg:" + delayTask.getMsg());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

   //生产者,将消息带上延迟时间,放入延迟队列
    private void producer(DelayQueue<DelayTask> delayTasks){
        DelayTask delayTask = new DelayTask(5000,"delay msg");
        System.out.println("消息放入时间:" + getCurrentTime());
        delayTasks.add(delayTask);
    }

    public static String getCurrentTime(){
        Date d = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(d);
    }
}

如上的代码

  • 生产者放入一个任务,延迟5秒执行
  • 消费者不停的轮询队列,从中拿任务来执行

从输出结果可以看出,任务过了5秒才被消费者拿到,实现了任务的延迟执行

消息放入时间:2020-06-04 00:06:17
消息消费时间:2020-06-04 00:06:22,msg:delay msg

rabbitMQ死信队列

rabbitMQ是用一种死信队列的方式来实现消息延迟消费的,就是消息先进入死信队列,此时的死信队列是没有消费者的,当到达过期时间后,消息会被转发到对应的业务队列中进行消费,从而达到任务延迟执行的效果
在这里插入图片描述

  • 生产者发送消息给死信Exchange,通过routing-key消息发送到指定的死信队列,此时死信队列是没有消费者的
  • 死信队列中的消息到期后会自动转发到业务Exchange中,通过routing-key消息发送到指定的业务队列中
  • 业务处理的Consumer监控业务队列,取到转发过来的消息进行消费,从而达到延迟队列的效果

下面来看一下代码实现:

首先是rabbitmq的配置

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: admin
    password: admin

rabbitmq的Configuration

@Configuration
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitMQConfig {
    private String host;
    private int port;
    private String username;
    private String password;


    public static String DELAY_EXCHANGE = "delay.exchange";//死信Exchange
    public static String BUSS_EXCHANGE = "buss.exchange";//业务Exchange
    public static String DELAY_QUEUE = "delay.queue";//死信队列
    public static String BUSS_QUEUE = "buss.queue";//业务队列

    /**
     * 定义一个死信队列
     * @return
     */
    @Bean
    public Queue delayQueue(){
        Map<String,Object> args = new HashMap<String,Object>();
        //消息过期后转发的exchange
        args.put("x-dead-letter-exchange",BUSS_EXCHANGE);
        //消息过期后转发的routing-key
        args.put("x-dead-letter-routing-key","delay.msg");
        //队列中消息的过期时间(注意消息上也可以设置过期时间),两者若同时设置取其小
        args.put("x-message-ttl",20000);
        return QueueBuilder.durable(DELAY_QUEUE).withArguments(args).build();
    }


    /**
     * 定义普通的业务队列
     * @return
     */
    @Bean
    public Queue bussQueue(){
        return new Queue(BUSS_QUEUE,true);
    }

    /**
     * 死信Exchange
     * @return
     */
    @Bean
    public TopicExchange delayTopicExchange(){
        return new TopicExchange(DELAY_EXCHANGE);
    }

    /**
     * 业务Exchange
     * @return
     */
    @Bean
    public TopicExchange bussTopicExchange(){
        return new TopicExchange(BUSS_EXCHANGE);
    }

    /**
     * 绑定死信队列与死信Exchange,设置routing-key为queue.delay
     * @return
     */
    @Bean
    public Binding bindingDelayExchangeMessage(){
        return BindingBuilder.bind(delayQueue()).to(delayTopicExchange()).with("queue.delay");
    }

    /**
     * 绑定业务队列与业务Exchange,设置routing-key为delay.msg
     * 注意:此处的rounting-key与死信队列的x-dead-letter-routing-key保持一致,才能保证死信消息过期后可以转发到此队列中
     * @return
     */
    @Bean
    public Binding bindingDelayMessage(){
        return BindingBuilder.bind(bussQueue()).to(bussTopicExchange()).with("delay.msg");
    }

    @Bean
    public ConnectionFactory connectionFactory(){
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host,port);
        connectionFactory.setAddresses(host);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost("/");
        return connectionFactory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
        return rabbitTemplate;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

定义一个消息实体

public class Message implements Serializable {
    private String key;
    private String value;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

定义一个Consumer来监控业务队列

@Component
@RabbitListener(queues = "buss.queue")
public class Consumer {

    @RabbitHandler
    public void consumerMessage(Message message){
        String key = message.getKey();
        String value = message.getValue();
        System.out.println("延迟队列消费时间" + getCurrentTime());
        System.out.println("消费的消息:" + message.getKey() + "---" + message.getValue());
    }

    public static String getCurrentTime(){
        Date d = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(d);
    }
}

最后写个Controller测试一下

@RestController
public class RabbitDemoTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("/test")
    public void send(){
        Message message = new Message();
        message.setKey("rabbit");
        message.setValue("Hello");
        System.out.println("消息发送时间:" + Consumer.getCurrentTime());
        rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE, "queue.delay", message, new MessagePostProcessor() {
            @Override
            public org.springframework.amqp.core.Message postProcessMessage(org.springframework.amqp.core.Message message) throws AmqpException {
                message.getMessageProperties().setContentEncoding("UTF-8");
                message.getMessageProperties().setExpiration("20000");
                return message;
            }
        });
    }
}

启动类

@SpringBootApplication
public class RabbitmqDemo {
    public static void main(String[] args){
        SpringApplication.run(RabbitmqDemo.class);
    }
}

我们访问:http://localhost:8080/test

执行结果如下:

消息发送时间:2020-06-04 22:16:01
延迟队列消费时间2020-06-04 22:16:21
消费的消息:rabbit---Hello

从执行结果可以看出来,是按指定的时间实现了消息的延迟消费

其实延迟队列的实现方式有很多,像时间轮、radis、Quartz、SchedulerX(阿里)等等,都可以实现延迟队列的功能

这些实现方案都各有千秋,我们在实际的项目中要根据情况来选择合适的实现方案,一切的技术方案都是为了解决业务问题

不要为了技术而技术,脱离业务的技术设计是耍流氓!!!

关注公众号,回复“源码333”可免费下载Demo源码
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/pzjtian/article/details/106560390