DelayQueue系列(二):基礎元件

逍遙jc發表於2018-12-24

原文發表於簡書DelayQueue之通用元件,本次將做部分優化調整。

在我們產品中有這麼一個場景,在醫生關閉問診的3min後,患者將無法繼續和醫生進行對話。我根據對業務的理解,和對技術實現成本的衡量,決定通過DelayQueue的方式來實現。

關於DelayQueue的相關內容介紹和核心原始碼解析已在上一篇DelayQueue系列(一):原始碼分析說明了。

根據我的經驗,我認為在生活中有如下場景可以用得到DelayQueue:
1.下單後一段時間(業內基本上都是30分鐘)內不付款,就自動取消訂單。
2.提交叫車申請後,一段時間內(比如說30秒)沒有附近的司機接單,就自動將該請求傳送給更多距離更遠的司機。

這類場景都有如下特點:
1.需要有一段時間的延遲,如果僅僅是為了非同步執行,那麼訊息佇列顯然是是更優的選擇。
2.對執行時間的精確度有一定要求,當然異常狀況下,也可以對精確度適當放寬鬆。比如場景1的訂單取消,規則設定為30分鐘不支付就取消,但實際場景中,精確到30分自然是最好結果,但假如出現故障,那麼在可允許的範圍內將訂單取消也是可以接受的(比如說將取消時間在放寬到32分鐘)。
3.執行是高頻率的。這點需要和第2點結合起來看,如果僅僅是為了低頻率的定時執行,個人認為任務排程可能是更優的選擇。

綜合來看,如果不需要延遲執行,那麼推薦用訊息佇列;如果對執行時間的精確度不那麼在意或執行頻率並不高,那麼推薦使用任務排程;如果需要延遲執行,且執行比較高頻,對執行時間的精確度有一定要求,可以考慮使用延遲佇列。 以上這些是我們為何採用DelayQueue來實現這個業務場景的原因。

為了方便使用DelayQueue,我封裝了元件對DelayQueue進行了擴充套件。
首先我定義了一個類TaskMessage,對Delayed進行了擴充套件,實現了compareTo和getDelay方法。 如下是TaskMessage類的核心程式碼。

public class TaskMessage implements Delayed {

    private String body;  //訊息內容
    private long executeTime;//執行時間
    private Function function;//具體執行方式
    
    public TaskMessage(Long delayTime,String body,  Function function) {
        this.body = body;
        this.function = function;
        this.executeTime = TimeUnit.NANOSECONDS.convert(delayTime, TimeUnit.MILLISECONDS) + System.nanoTime();
    }

    @Override
    public int compareTo(Delayed delayed) {
        TaskMessage msg = (TaskMessage) delayed;
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) -msg.getDelay(TimeUnit.MILLISECONDS));
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.executeTime - System.nanoTime(), TimeUnit.NANOSECONDS);
    }
}
複製程式碼

外部呼叫只需要TaskMessage m1 = new TaskMessage(delayTime, body, function)就可以生成一個延遲任務的元素了,內部自動就根據延遲時間計算出這個延遲任務元素的預期執行時間。

Function是1.8版引入的函式式介面,主要方法是R apply(T t),功能是將Function物件應用到輸入的引數上,然後返回計算結果。 那麼達到延遲任務的預期執行時間時,只需要呼叫一下function.apply()方法就可以了,不需要關心apply的具體實現。apply的具體實現方法是在呼叫時才明確的。

然後定義一個延遲任務的執行執行緒類TaskConsumer,實現了Runnable,重寫了run方法。因為延遲任務的執行,必然是需要重新起執行緒去執行的,不能阻礙主執行緒的操作。

如下是TaskConsumer類的核心程式碼。

public class TaskConsumer implements Runnable {

    // 延時佇列
    private DelayQueue<TaskMessage> queue;

    //用於標記處理任務執行緒的id
    private int threadId;
    
    //訊號量
    private Boolean signal;

    TaskConsumer(DelayQueue<TaskMessage> queue, int threadId) {
        this.queue = queue;
        this.threadId = threadId;
        this.signal = Boolean.TRUE;
    }

    void finish() {
        this.signal = Boolean.FALSE;
    }
	 	
    @Override
    public void run() {
        while (signal) {
            try {
                TaskMessage take = queue.take();
                if (logger.isInfoEnabled()) {
                    logger.info("處理執行緒的id為" + threadId + ",消費訊息內容為:" + take.getBody() + ",預計執行時間為" +
                            DateFormatUtils.timeStampToString(take.getDelay(TimeUnit.MILLISECONDS) + System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss"));
                }
                take.getFunction().apply(take.getBody());
            } catch (InterruptedException e) {
                if (logger.isInfoEnabled()) {
                    logger.info("id為" + threadId + "的處理執行緒被強制中斷");
                }
            } catch (Exception e) {
                logger.error("taskConsumer error", e);
            }
        }
        if (logger.isInfoEnabled()) {
            logger.info("id為" + threadId + "的處理執行緒已停止");
        }
    }
}
複製程式碼

這個類核心程式碼就只有如下兩行。
TaskMessage take = queue.take();
獲取延遲佇列的隊首元素。前文已經解釋過,Queue的take方法會返回佇列的隊首元素,否則就會掛起執行緒。所以只要有返回值,必然就能獲取到當前需要執行的TaskMessage元素。

take.getFunction().apply(take.getBody());
執行延遲任務元素的apply方法。applay方法是在定義TaskMessage的時候確定的,表明了到達預期執行時間所需要進行的一系列操作,那麼此時只需要執行對應的apply方法就可以了。

最後是載入TaskConsumer的統一管理類TaskManager。
如下是TaskManager類的核心程式碼。

public class TaskManager implements InitializingBean,DisposableBean{

    @Override
    public void afterPropertiesSet() {
        for (int i = 0; i < threadCount; i++) {
            TaskConsumer taskConsumer = new TaskConsumer(queue, i);
            taskConsumerList.add(taskConsumer);
            Thread thread = new Thread(taskConsumer);
            threadList.add(thread);
            thread.start();
        }
    }

    @Override
    public void destroy() {
        for (int i = 0; i < threadList.size(); i++) {
            threadList.get(i).interrupt();
            taskConsumerList.get(i).finish();
        }
    }
}
複製程式碼

這個類的作用在於初始化類後,就啟動執行緒不斷的去獲取延遲任務。然後在銷燬類後,先中斷消費者執行緒,然後設定訊號量使得消費者執行緒的run方法能跳出死迴圈,從而使得消費執行緒正常結束。

最後是如何呼叫的示例。很簡單,就只有兩步:
1、生成延遲任務元素taskMessage
2、將taskMessage新增到延遲佇列中

TaskMessage taskMessage = new TaskMessage(delayTime * 1000, messageBody,
        function -> this.processTask(delayTaskMessage));
DelayQueue<TaskMessage> queue = taskManager.getQueue();
queue.offer(taskMessage);
複製程式碼

ok,以上是如何擴充套件DelayQueue的功能構造成高可用的元件的方案,歡迎大家來一起討論。

下一章我準備講一下我們專案中運用DelayQueue的過程中碰到的問題以及如何持久化的方案。

相關文章