大家肯定都有過在餓了么,或者在美團外賣下單的經歷,下完單后,超過一定的時間,訂單就被自動取消了。這就是延時任務。延時任務的應用場景相當廣泛,不僅僅上面所說的餓了嗎,美團外賣,還有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。
這種方式也比較方便,而且幾乎沒有延遲,對內存占用也不大,因為畢竟只是存放一個訂單號而已。
缺點也比較明顯,因為訂單是存放在內存的,一旦服務器掛了,就麻煩了。消費者和生產者只能在同一套代碼中,現在是微服務的時代,一般來說消費者和生產者都是分開的,甚至是在不同的服務器。因為這樣,如果消費者壓力過大,可以通過加服務器的方式很方便的來解決。
前三種方式也可以結合在一起使用