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

大家肯定都有过在饿了么,或者在美团外卖下单的经历,下完单后,超过一定的时间,订单就被自动取消了。这就是延时任务。延时任务的应用场景相当广泛,不仅仅上面所说的饿了吗,美团外卖,还有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。

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,232评论 4 56
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,893评论 2 89
  • 我知道这次跟以前不一样,他不会再原谅我,我也没有机会了。想想,我们从4月底开始闹,现在是六月底,两个月,中间和好了...
    自己找鱼的喵喵阅读 185评论 0 0
  • ionic2 ionic2cordova + angular2 + ionic2 评价 开发上手还是比较简单的主要...
    qppq54s阅读 447评论 0 0