幾種實現延時任務的方式(一)

你們確定都有過在餓了麼,或者在美團外賣下單的經歷,下完單後,超過必定的時間,訂單就被自動取消了。這就是延時任務。延時任務的應用場景至關普遍,不單單上面所說的餓了嗎,美團外賣,還有12306,或者是淘寶,攜程等等 都有這樣的場景。這延時任務是怎麼實現的呢?跟着我,繼續看下去吧。數據庫

1.在SQL查詢,Serive層組裝的時候作手腳

在拼接SQL或者Serive層作一些判斷,好比 訂單狀態爲 「已下單,但未支付」,同時 當前時間超過了 下單時間 15分鐘,顯示在用戶端或者後臺的訂單狀態就改成 「已取消」。bash

這種方式比較方便,也沒有任何延遲,可是數據庫裏面的狀態不是真實狀態了。若是須要提供接口給其餘部門調用的話,別忘了對這個訂單狀態作一些特殊處理。服務器

2.Job

這是最普通的方式之一了。就是開一個Job,每隔一段時間去循環訂單,當知足條件後,修改訂單狀態。ide

這種方式也比較方便,可是會有必定的延遲,若是訂單數據比較少的話,每分鐘掃描一次,仍是能夠接受的,延遲也就在一分鐘左右。可是訂單數據一旦大了起來,可能一小時也掃描不完,那麼延遲就至關恐怖了。並且不停的掃描數據庫,對於數據庫也是一種壓力。 固然還能夠作一些改進,好比掃描的時候加上時間範圍,在必定時間之前的訂單不掃描了,由於這些訂單已經被上一次運行的Job給處理了。微服務

第一種方式能夠和第二種方式結合起來使用。測試

前面兩個是比較常規的作法,若是數據量不大,使用起來,也不錯。ui

3.DelayQueue

DelayQueue是Java自帶隊列,從名字就能夠知道它是一個延遲隊列。 this

image.png
從上面的圖能夠知道DelayQueue是一個泛型隊列,它接受的類型是繼承Delayed的。也就是咱們須要寫一個類去繼承(實現)Delayed。實現Delayed,須要重寫兩個方法:

public long getDelay(TimeUnit unit)
 public int compareTo(Delayed o)
複製代碼

第一個方法:消息是否到期(是否能夠被讀取出來)判斷的依據。當返回負數,說明消息已到期,此時消息就能夠被讀取出來了。spa

第二個方法:往DelayQueue裏面塞入數據會執行這個方法,是數據應該排在哪一個位置的判斷依據。3d

在這個類裏面,咱們須要定義一些屬性,好比 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。

這種方式也比較方便,並且幾乎沒有延遲,對內存佔用也不大,由於畢竟只是存放一個訂單號而已。 缺點也比較明顯,由於訂單是存放在內存的,一旦服務器掛了,就麻煩了。消費者和生產者只能在同一套代碼中,如今是微服務的時代,通常來講消費者和生產者都是分開的,甚至是在不一樣的服務器。由於這樣,若是消費者壓力過大,能夠經過加服務器的方式很方便的來解決。

前三種方式也能夠結合在一塊兒使用

相關文章
相關標籤/搜索