上一篇文章中提到了我們在項目中運用DelayQueue解決了一些需要延遲執行的任務,但是最近我們在生產環境上遇到了一個問題。重啟服務器后,那些未執行的延遲任務就消失不見了。于是如何將延遲任務持久化就提上了日程。
關于DelayQueue的具體實現方案,已經在上一篇文章DelayQueue之通用組件中提到過了。本文就不再復述了。
本期的主題主要是探討如何將延遲任務進行持久化。
何為延遲任務的持久化?顧名思義,就是將這些延遲任務的執行必要的數據,存儲到數據庫或redis等。
那么為何要進行持久化呢?很簡單,因為延遲任務的數據是放在內存里,那么自己需要我們自己寫持久化的備案以達到高可用,否則服務器故障宕機或新發布版本而導致服務器重啟的時候,那么那些未執行的延遲任務數據將徹底丟失,這顯然是我們不愿意見到的。
我目前采用的方案如下:
1、在需要使用到DelayQueue的地方,調用saveDelayTask方法,需要的參數有延遲任務函數策略工廠類的路由tag,執行方法所需的json格式的參數messageBody,延遲多久執行以秒為單位的delayTime。
2、任務調度每15秒去執行getNotCompletedMessageList方法。
大多數情況下,會在預計執行的時間點準時去執行processTask方法,那么異常狀況下,如果服務器重啟,那么定時任務調度會在一定時間后找到那些沒有如期執行的延遲任務,通過定時任務調度的方式依次執行各自任務的processTask方法。
異常狀態下,延遲任務執行會比預期執行時間有一定的延后,我設計的方案是目前我們可以允許的范圍,這個大家可以酌情設置備選方案延后的時間。
核心代碼如下,其他代碼很簡單就不一一公布了。
public void saveDelayTask(String tag, String messageBody, Long delayTime) {
DelayTaskMessage delayTaskMessage = new DelayTaskMessage();
delayTaskMessage.setTag(tag);
LocalDateTime now = LocalDateTime.now();
delayTaskMessage.setCreateTime(now);
delayTaskMessage.setUpdateTime(now);
delayTaskMessage.setDelayTime(delayTime);
delayTaskMessage.setExpectedTime(now.plusSeconds(delayTime));
delayTaskMessage.setMessageBody(messageBody);
delayTaskMessage.setStatus(KafkaMessageStatusEnum.NOT_COMPLETE.getCode());
int res = delayTaskMessageMapper.insertDelayTaskMessage(delayTaskMessage);
if (res <= 0) {
log.error("ybBrokerApp|insertDelayTaskMessage error, res<=0");
throw new RuntimeException("insertDelayTaskMessage error, res<=0");
}
TaskMessage taskMessage = new TaskMessage(delayTime * 1000, messageBody,
function -> this.processTask(delayTaskMessage));
DelayQueue<TaskMessage> queue = taskManager.getQueue();
queue.offer(taskMessage);
}
首先來分析一下,用來保存延遲任務的saveDelayTask方法。
tag是指延遲任務的標記,用于指定對應的策略類。
messageBody主要用于存儲執行延遲任務的一些必要的數據,以json方法存儲。
delayTime是延遲時間,默認以s為單位,主要是便于使用。
這個方法的主要功能是首先保存還未執行的延遲任務,自動根據延遲時間計算該延遲任務的預期執行時間,以便于后續的補償算法跟蹤,然后運用DelayQueue的特性,將這個延遲任務提交給延遲隊列執行。
public int processTask(DelayTaskMessage param) {
DelayTaskMessage delayTaskMessage = delayTaskMessageMapper.getDelayTaskMessageById(param.getId());
try {
if (null != delayTaskMessage && !Objects.equals(delayTaskMessage.getStatus(), KafkaMessageStatusEnum.NOT_COMPLETE.getCode())) {
log.info("processTask executed already");
return 1;
}
if (null != delayTaskMessage) {
DelayTaskExecuteProcessor processor = processorFactory.getExecuteProcessor(delayTaskMessage.getTag());
if (processor != null) {
processor.execute(delayTaskMessage);
} else {
throw new RuntimeException("no such processor,tag=" + delayTaskMessage.getTag());
}
delayTaskMessage.setStatus(KafkaMessageStatusEnum.COMPLETE.getCode());
delayTaskMessage.setExecutionTime(LocalDateTime.now());
try {
delayTaskMessage.setIpAddress(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException ex) {
log.error("Address.getLocalHost error", ex);
}
int res = delayTaskMessageMapper.updateDelayTaskMessageStatus(delayTaskMessage);
if (res <= 0) {
log.error("updateDelayTaskMessageStatus error res<=0");
throw new RuntimeException("updateDelayTaskMessageStatus error");
}
return 1;
} else {
log.error("ybBrokerApp processTask error, delayTaskMessage is null delayTaskMessageId=", param.getId());
return 0;
}
} catch (Exception e) {
log.error("ybBrokerApp processTask error , param = " + param.toString() + "|", e);
if (null != delayTaskMessage) {
delayTaskMessage.setStatus(KafkaMessageStatusEnum.FAIL.getCode());
delayTaskMessage.setErrorStack(e.getMessage());
try {
delayTaskMessage.setIpAddress(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException ex) {
log.error("Address.getLocalHost error", ex);
}
delayTaskMessageMapper.updateDelayTaskMessageStatus(delayTaskMessage);
}
return 0;
}
}
然后是核心的處理延遲任務的processTask方法。
1、根據id,在數據庫尋找到對應需要執行的延遲任務的持久化數據。
2、如果這條持久化數據非空且狀態不是未執行的狀態,那么提示該任務已經被執行過,防止重復執行。這里的status,主要有三種狀態,未執行,已執行成功和執行失敗。
3、如果這條持久化數據非空,且是未執行的狀態,那么找到tag對應的策略類執行對應的execute方法。
4、將執行方法的ip地址記錄下來,便于后續分析,同時將這條持久化數據的狀態改為已執行成功的狀態。
5、如果執行失敗,將ip地址記錄下來,同時把數據的狀態改為執行失敗。
在第4步中為何要先執行方法,后改狀態呢?我是這么想的,延遲任務的執行方法是外部寫的,在組件設計的時候無法控制會不會出現異常,而修改持久化數據的狀態的方法是可控的,所以從這個角度上,我覺得先執行方法再修改狀態更合理些。
需要注意的是,這個方法沒有考慮并發的情況,是因為我在補償方案里又額外延遲了一段時間,出現并發的情況非常小,不是很有必要考慮這種情況。
public List<DelayTaskMessage> getNotCompletedMessageList(int total, int index) {
LocalDateTime expectedTime = LocalDateTime.now().plusSeconds(15L);
List<DelayTaskMessage> delayTaskMessageList = delayTaskMessageMapper.getNotCompletedMessageList(expectedTime,total, index);
if (CollectionUtils.isEmpty(delayTaskMessageList)) {
return Lists.newArrayList();
}
return delayTaskMessageList;
}
最后是補償方案的落實,我是在定時任務中去保證延遲任務一定會被執行至少一次的。
我的設計是每隔15s去遍歷一下那些過了預期執行時間+15s依然未執行的的延遲任務。然后將這些列表中的延遲任務重新調用processTask方法。
如果最終是通過補償方案執行的延遲任務會比預期執行時間還要晚執行15到30s。目前在我們的項目中,這個額外延遲是可以被接收的。大家還是要根據實際情況酌情修改這個額外延遲的時間。
以上是我針對DelayQueue設計的的持久化方案,如果大家有更好的意見,可以一起討論哦。