欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

几种实现延时任务的方式(一)

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

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

1.在sql查询,serive层组装的时候做手脚

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

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

2.job

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

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

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

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

3.delayqueue

delayqueue是java自带队列,从名字就可以知道它是一个延迟队列。
几种实现延时任务的方式(一)
从上面的图可以知道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方法的用处:
根据此方法的返回值判断数据应该排在哪个位置
几种实现延时任务的方式(一)
一般来说,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();
            }
        }
    }

运行:
几种实现延时任务的方式(一)

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

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

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