• 几种实现延时任务的方式


    大家肯定都有过在饿了么,或者在美团外卖下单的经历,下完单后,超过一定的时间,订单就被自动取消了。这就是延时任务。延时任务的应用场景相当广泛,不仅仅上面所说的饿了吗,美团外卖,还有12306,或者是淘宝,携程等等 都有这样的场景。这延时任务是怎么实现的呢?跟着我,继续看下去吧。

    1.在SQL查询,Serive层组装的时候做手脚

    在拼接SQL或者Serive层做一些判断,比如 订单状态为 “已下单,但未支付”,同时 当前时间超过了 下单时间 15分钟,显示在用户端或者后台的订单状态就改为 “已取消”。

    这种方式比较方便,也没有任何延迟,但是数据库里面的状态不是真实状态了。如果需要提供接口给其他部门调用的话,别忘了对这个订单状态做一些特殊处理。

    2.Job

    这是最普通的方式之一了。就是开一个Job,每隔一段时间去循环订单,当满足条件后,修改订单状态。

    这种方式也比较方便,但是会有一定的延迟,如果订单数据比较少的话,每分钟扫描一次,还是可以接受的,延迟也就在一分钟左右。但是订单数据一旦大了起来,可能一小时也扫描不完,那么延迟就相当恐怖了。而且不停的扫描数据库,对于数据库也是一种压力。
    当然还可以做一些改进,比如扫描的时候加上时间范围,在一定时间以前的订单不扫描了,因为这些订单已经被上一次运行的Job给处理了。

    第一种方式可以和第二种方式结合起来使用。

    前面两个是比较常规的做法,如果数据量不大,使用起来,也不错。

    3.DelayQueue

    DelayQueue是Java自带队列,从名字就可以知道它是一个延迟队列。
    image.png
    从上面的图可以知道DelayQueue是一个泛型队列,它接受的类型是继承Delayed的。也就是我们需要写一个类去继承(实现)Delayed。实现Delayed,需要重写两个方法:

     public long getDelay(TimeUnit unit)
     public int compareTo(Delayed o)

    第一个方法:消息是否到期(是否可以被读取出来)判断的依据。当返回负数,说明消息已到期,此时消息就可以被读取出来了。

    第二个方法:往DelayQueue里面塞入数据会执行这个方法,是数据应该排在哪个位置的判断依据。

    在这个类里面,我们需要定义一些属性,比如 orderId,orderTime(下单时间),expireTime(延期时间)。

    现在我们先来做一个测试,测试compareTo方法:

    public class OrderDelay implements Delayed {
    
        private int orderId;
    
        private Date orderTime;
    
        public Date getOrderTime() {
            return orderTime;
        }
    
        public void setOrderTime(Date orderTime) {
            this.orderTime = orderTime;
        }
    
        private static final int expireTime = 15000;
    
        public int getOrderId() {
            return orderId;
        }
    
        public void setOrderId(int orderId) {
            this.orderId = orderId;
        }
    
        @Override
        public long getDelay(TimeUnit unit) {
            return orderTime.getTime() + expireTime - new Date().getTime();
        }
    
        @Override
        public int compareTo(Delayed o) {
            return this.orderTime.getTime() - ((OrderDelay) o).orderTime.getTime() > 0 ? 1 : -1;
        }
    }

    getDelay方法可以暂时不看,因为测试compareTo还不需要用到这方法。
    然后我们在main方法写一些代码:

            DelayQueue<OrderDelay> queue = new DelayQueue<>();
            Calendar c = Calendar.getInstance();
            c.add(Calendar.DATE, 1);
    
            Date time1 = c.getTime();
            OrderDelay orderDelay1=new OrderDelay();
            orderDelay1.setOrderId(1);
            orderDelay1.setOrderTime(time1);
            queue.put(orderDelay1);
            System.out.println("1: "+ new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(time1));
    
            c.add(Calendar.DATE, -15);
            Date time2 = c.getTime();
            OrderDelay orderDelay2=new OrderDelay();
            orderDelay2.setOrderId(2);
            orderDelay2.setOrderTime(time2);
            queue.put(orderDelay2);
    
            System.out.println("2: "+ new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(time2));
            int a=0;

    把断点设置在最后一行,然后调试,你会发现 虽然 order1是先push到DelayQueue的,但是DelayQueue第一条数据却是order2的,这就是compareTo方法的用处:
    根据此方法的返回值判断数据应该排在哪个位置
    image.png
    一般来说,orderTime越小的,肯定越先过期,越先被消费,所以这个方法是没有问题的。

    compareTo测试完成了,让我们把代码补充完整,再测试下getDelay这个方法吧(这个时候,你需要注意getDelay方法里面的代码了):
    首先定义一个生产者方法:

     private static void produce(int orderId) {
            OrderDelay delay = new OrderDelay();
            delay.setOrderId(orderId);
            Date currentTime = new Date();
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = formatter.format(currentTime);
            delay.setOrderTime(currentTime);
            System.out.printf("现在时间是%s;订单%d加入队列%n", dateString, orderId);
            queue.put(delay);
        }

    再定义一个消费者方法:

     private static void consum() {
            while (true) {
                try {
                    OrderDelay orderDelay = queue.take();//
                    Date currentTime = new Date();
                    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    String dateString = formatter.format(currentTime);
                    System.out.printf("现在时间是%s;订单%d过期%n", dateString, orderDelay.getOrderId());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    在main方法里面运行这两个方法:

    produce(1);
    consum();

    再把断点设置在

     OrderDelay orderDelay = queue.take();

    调试,运行到这里,F8,你会发现代码执行不下去了,被阻塞了,其实这也说明了DelayQueue是一个阻塞队列。15秒后,终于进入了下一行代码,并且拿到了数据,这就是getDelay和take方法的用处了。
    getDelay:根据方法的返回值,判断数据可否被take出来。
    take:取出数据,但是受到getDelay方法的制约,如果没有满足条件,则会阻塞。

    好了。getDelay方法和compareTo都已经测试完毕了。下面的事情就简单了。
    我就直接放出代码了:

       static DelayQueue<OrderDelay> queue = new DelayQueue<>();
    
        public static void main(String[] args) throws InterruptedException {
            Thread productThread = new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(1200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    produce(i);
                }
            });
            productThread.start();
    
    
            Thread consumThread = new Thread(() -> {
                consum();
            });
            consumThread.start();
        }
    
        private static void produce(int orderId) {
            OrderDelay delay = new OrderDelay();
            delay.setOrderId(orderId);
            Date currentTime = new Date();
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = formatter.format(currentTime);
            delay.setOrderTime(currentTime);
            System.out.printf("现在时间是%s;订单%d加入队列%n", dateString, orderId);
            queue.put(delay);
        }
    
        private static void consum() {
            while (true) {
                try {
                    OrderDelay orderDelay = queue.take();//
                    Date currentTime = new Date();
                    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    String dateString = formatter.format(currentTime);
                    System.out.printf("现在时间是%s;订单%d过期%n", dateString, orderDelay.getOrderId());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    运行:
    image.png

    通过控制台输出,你会发现功能实现OK。

    这种方式也比较方便,而且几乎没有延迟,对内存占用也不大,因为毕竟只是存放一个订单号而已。
    缺点也比较明显,因为订单是存放在内存的,一旦服务器挂了,就麻烦了。消费者和生产者只能在同一套代码中,现在是微服务的时代,一般来说消费者和生产者都是分开的,甚至是在不同的服务器。因为这样,如果消费者压力过大,可以通过加服务器的方式很方便的来解决。

    前三种方式也可以结合在一起使用

  • 相关阅读:
    剑指offer编程题Java实现——面试题14调整数组顺序使奇数位于偶数之前
    剑指offer编程题Java实现——面试题13在O(1)时间内删除链表节点
    剑指offer编程题Java实现——面试题12相关题大数的加法、减法、乘法问题的实现
    剑指offer编程题Java实现——面试题12打印1到最大的n位数
    《深入理解JAVA虚拟机》——学习笔记
    剑指offer编程题Java实现——面试题11数值的整数次方
    剑指offer编程题Java实现——面试题10二进制中1的个数
    剑指offer编程题Java实现——面试题9斐波那契数列
    剑指offer编程题Java实现——面试题8旋转数组的最小数字
    mysql高可用集群——MHA架构
  • 原文地址:https://www.cnblogs.com/niudaxianren/p/10043036.html
Copyright © 2020-2023  润新知