實現延時任務有很多的方法,網上關於延時任務的實現的文章已經不少了。比如:實現延時任務的10種方法等等。但是這些文章基本上都是將方法大概的列舉一下,給出部分示例代碼,對於有經驗的老程式員可能一看就知道該怎麼去把它實現完整,但是對於初學者來說不夠友好。所以,我打算寫一個系列的文章,詳細的給出每種延時任務 ...
實現延時任務有很多的方法,網上關於延時任務的實現的文章已經不少了。比如:實現延時任務的10種方法等等。但是這些文章基本上都是將方法大概的列舉一下,給出部分示例代碼,對於有經驗的老程式員可能一看就知道該怎麼去把它實現完整,但是對於初學者來說不夠友好。所以,我打算寫一個系列的文章,詳細的給出每種延時任務的實現方法、完整實現代碼,以及工作原理,歡迎並期待大家關註我。
小概念:什麼是延時任務?舉個例子:你買了一張火車票,必須在30分鐘之內付款,否則該訂單被自動取消。訂單30分鐘不付款自動取消,這個任務就是一個延時任務。
一、DelayQueue的應用原理
DelayQueue是一個無界的BlockingQueue的實現類,用於放置實現了Delayed介面的對象,其中的對象只能在其到期時才能從隊列中取走。
- BlockingQueue即阻塞隊列,java提供的面向多線程安全的隊列數據結構,當隊列內元素數量為0的時候,試圖從隊列內獲取元素的線程將被阻塞或者拋出異常。
- 這裡的“無界”隊列,是指隊列的元素數量不存在上限,隊列的容量會隨著元素數量的增加而擴容。
DelayQueue實現了BlockingQueue介面,所以具有無界、阻塞的特點,除此之外它自己的核心特點就是:
- 放入該隊列的延時任務對象,只要到達延時時間之後才能被取到。
- DelayQueue 不接收null元素
- DelayQueue 只接受那些實現了java.util.concurrent.Delayed介面的對象
二、訂單延時任務的實現
瞭解了DelayQueue的特點之後,我們就可以利用它來實現延時任務了,實現java.util.concurrent.Delayed
介面。
import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* 延時訂單任務
*/
public class OrderDelayObject implements Delayed {
private String name;
private long delayTime; //延時時間
//實際業務中這裡傳訂單信息對象,我這裡只做demo,所以使用字元串了
private String order;
public OrderDelayObject(String name, long delayTime, String order) {
this.name = name;
//延時時間加上當前時間
this.delayTime = System.currentTimeMillis() + delayTime;
this.order = order;
}
//獲取延時任務的倒計時時間
@Override
public long getDelay(TimeUnit unit) {
long diff = delayTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
//延時任務隊列,按照延時時間元素排序,實現Comparable介面
@Override
public int compareTo(@NotNull Delayed obj) {
return Long.compare(this.delayTime, ((OrderDelayObject) obj).delayTime);
}
@Override
public String toString() {
Date date = new Date(delayTime);
SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return "\nOrderDelayObject:{"
+ "name=" + name
+ ", time=" + sd.format(date)
+ ", order=" + order
+ "}";
}
}
- 上文類中的order為訂單信息對象,在實際的業務開發過程中應該是傳遞訂單信息,用於取消訂單業務的實現(訂單30分鐘不付款自動取消)。
- Delayed介面繼承自 Comparable介面,所以需要實現compareTo方法,用於延時任務在隊列中按照“延時時間”進行排序。
- getDelay方法是Delayed介面方法,實現該方法提供獲取延時任務的倒計時時間
三、訂單處理
首先我們需要一個容器,永久保存延時任務隊列,如果是Spring開發環境我們可以這樣做。
@Bean("orderDelayQueue")
public DelayQueue<OrderDelayObject> orderDelayQueue(){
return new DelayQueue<OrderDelayObject>();
}
當用戶下單的時候,將訂單下單任務放入延時隊列
@Resource
private DelayQueue<OrderDelayObject> orderDelayQueue;
//發起訂單下單的時候將訂單演示對象放入orderDelayQueue
orderDelayQueue.add(
new OrderDelayObject(
"訂單延時取消任務",
30 * 60 * 1000, //延時30分鐘
"延時任務訂單對象信息"
)
);
系統內開啟一個線程,不斷的從隊列中獲取消息,獲取到之後對延時消息進行處理。DelayQueue
的take方法從隊列中獲取延時任務對象,如果隊列元素數量為0,或者沒有到達“延時時間的任務”,該線程會被阻塞。
@Component
public class DelayObjectConsumer implements InitializingBean {
@Resource
private DelayQueue<OrderDelayObject> orderDelayQueue;
@Override
public void afterPropertiesSet() throws Exception {
while (true) {
OrderDelayObject task = orderDelayQueue.take();
System.out.println(task.toString());
System.out.println(task.getOrder());
//根據order訂單信息,去查詢該訂單的支付信息
//如果用戶沒有進行支付,將訂單從資料庫中關閉
//如果訂單併發量比較大,這裡可以考慮非同步或線程池的方式進行處理
}
}
}
需要說明的是,這裡的while-true迴圈的延時任務處理時順序執行的,在訂單併發量比較大的時候,需要考慮非同步處理的方式完成訂單的關閉操作。我之前寫作一個SpringBoot的可觀測、易配置的線程池開源項目,可能會對你有幫助,源代碼地址:https://gitee.com/hanxt/zimug-monitor-threadpool
經過我的測試,放入orderDelayQueue的延時任務,在半小時之後得到正確的執行處理。說明我們的實現是正確的。
四、優缺點
使用DelayQueue實現延時任務非常簡單,而且簡便,全部都是標準的JDK代碼實現,不用引入第三方依賴(不依賴redis實現、消息隊列實現等),非常的輕量級。
它的缺點就是所有的操作都是基於應用記憶體的,一旦出現應用單點故障,可能會造成延時任務數據的丟失。如果訂單併發量非常大,因為DelayQueue是無界的,訂單量越大,隊列內的對象就越多,可能造成OOM的風險。所以使用DelayQueue實現延時任務,只適用於任務量較小的情況。
歡迎關註我的公告號:字母哥雜談,回覆003贈送作者專欄《docker修煉之道》的PDF版本,30餘篇精品docker文章。字母哥博客:zimug.com