參考:juejin.cn/post/6930912870058328071
# 單點定時任務
JDK原生
自從JDK1.5之后,提供了ScheduledExecutorService代替TimerTask來執行定時任務,提供了不錯的可靠性。
public class SomeScheduledExecutorService {public static void main(String[] args) {// 創建任務隊列,共 10 個線程ScheduledExecutorService scheduledExecutorService =Executors.newScheduledThreadPool(10);// 執行任務: 1秒 后開始執行,每 30秒 執行一次scheduledExecutorService.scheduleAtFixedRate(() -> {System.out.println("執行任務:" + new Date());}, 10, 30, TimeUnit.SECONDS);}}
Spring Task
Spring Framework自帶定時任務,提供了cron表達式來實現豐富定時任務配置。新手推薦使用https://cron.qqe2.com/這個網站來匹配你的cron表達式。
public class SomeJob {private static final Logger LOGGER = LoggerFactory.getLogger(SomeJob.class);/*** 每分鍾執行一次(例:18:01:00,18:02:00)* 秒 分鍾 小時 日 月 星期 年*/public void someTask() {//...}}
單點的定時服務在目前微服務的大環境下,應用場景越來越局限,所以嘗鮮一下分布式定時任務吧。
基於 Redis 實現
相較於之前兩種方式,這種基於Redis的實現可以通過多點來增加定時任務,多點消費。但是要做好防范重復消費的准備。
通過ZSet的方式
將定時任務存放到ZSet集合中,並且將過期時間存儲到ZSet的Score字段中,然后通過一個循環來判斷當前時間內是否有需要執行的定時任務,如果有則進行執行。
具體實現代碼如下:
/*** Description: 基於Redis的ZSet的定時任務 .<br>** @author mxy* @Date 2020/8/25 11:54*/public class RedisJob {public static final String JOB_KEY = "redis.job.task";private static final Logger LOGGER = LoggerFactory.getLogger(RedisJob.class);/*** 添加任務.** @param task*/public void addTask(String task, Instant instant) {stringRedisTemplate.opsForZSet().add(JOB_KEY, task, instant.getEpochSecond());}/*** 定時任務隊列消費* 每分鍾消費一次(可以縮短間隔到1s)*/public void doDelayQueue() {long nowSecond = Instant.now().getEpochSecond();// 查詢當前時間的所有任務Set<String> strings = stringRedisTemplate.opsForZSet().range(JOB_KEY, 0, nowSecond);for (String task : strings) {// 開始消費 taskLOGGER.info("執行任務:{}", task);}// 刪除已經執行的任務stringRedisTemplate.opsForZSet().remove(JOB_KEY, 0, nowSecond);}}
適用場景如下:
-
訂單下單之后15分鍾后,用戶如果沒有付錢,系統需要自動取消訂單。
-
紅包24小時未被查收,需要延遲執退還業務;
-
某個活動指定在某個時間內生效&失效;
優勢是:
-
省去了MySQL的查詢操作,而使用性能更高的Redis做為代替;
-
不會因為停機等原因,遺漏要執行的任務;
鍵空間通知的方式
我們可以通過Redis的鍵空間通知來實現定時任務,它的實現思路是給所有的定時任務設置一個過期時間,等到了過期之后,我們通過訂閱過期消息就能感知到定時任務需要被執行了,此時我們執行定時任務即可。
默認情況下Redis是不開啟鍵空間通知的,需要我們通過config set notify-keyspace-events Ex的命令手動開啟。
開啟之后定時任務的代碼如下:
自定義監聽器
/*** 自定義監聽器.*/public class KeyExpiredListener extends KeyExpirationEventMessageListener {public KeyExpiredListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}public void onMessage(Message message, byte[] pattern) {// channelString channel = new String(message.getChannel(), StandardCharsets.UTF_8);// 過期的keyString key = new String(message.getBody(), StandardCharsets.UTF_8);// todo 你的處理}}
設置該監聽器
/*** Description: 通過訂閱Redis的過期通知來實現定時任務 .<br>** @author mxy* @Date 2020/8/25 12:07*/public class RedisExJob {public RedisMessageListenerContainer redisMessageListenerContainer() {RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);return redisMessageListenerContainer;}public KeyExpiredListener keyExpiredListener() {return new KeyExpiredListener(this.redisMessageListenerContainer());}}
Spring會監聽符合以下格式的Redis消息
private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*");
基於Redis的定時任務能夠適用的場景也比較有限,但實現上相對簡單,但對於功能冪等有很大要求。從使用場景上來說,更應該叫做延時任務。
場景舉例:
-
訂單下單之后15分鍾后,用戶如果沒有付錢,系統需要自動取消訂單。
-
紅包24小時未被查收,需要延遲執退還業務;
優劣勢是:
-
被動觸發,對於服務的資源消耗更小;
-
Redis的Pub/Sub不可靠,沒有ACK機制等,但是一般情況可以容忍;
-
鍵空間通知功能會耗費一些CPU
# 分布式定時任務
引入分布式定時任務組件or中間件
將定時任務作為單獨的服務,遏制了重復消費,獨立的服務也有利於擴展和維護。
quartz
依賴於MySQL,使用相對簡單,可多節點部署,通過競爭數據庫鎖來保證只有一個節點執行任務。沒有圖形化管理頁面,使用相對麻煩。
elastic-job-lite
依賴於Zookeeper,通過zookeeper的注冊與發現,可以動態的添加服務器。
-
多種作業模式
-
失效轉移
-
運行狀態收集
-
多線程處理數據
-
冪等性
-
容錯處理
-
支持spring命名空間
-
有圖形化管理頁面
LTS
依賴於Zookeeper,集群部署,可以動態的添加服務器。可以手動增加定時任務,啟動和暫停任務。
-
業務日志記錄器
-
SPI擴展支持
-
故障轉移
-
節點監控
-
多樣化任務執行結果支持
-
FailStore容錯
-
動態擴容
-
對spring相對友好
-
有監控和管理圖形化界面
xxl-job
國產,依賴於MySQL,基於競爭數據庫鎖保證只有一個節點執行任務,支持水平擴容。可以手動增加定時任務,啟動和暫停任務。
-
彈性擴容
-
分片廣播
-
故障轉移
-
Rolling實時日志
-
GLUE(支持在線編輯代碼,免發布)
-
任務進度監控
-
任務依賴
-
數據加密
-
郵件報警
-
運行報表
-
優雅停機
-
國際化(中文友好)
# 總結
微服務下,推薦使用xxl-job這一類組件服務將定時任務合理有效的管理起來。而單點的定時任務有其局限性,適用於規模較小、對未來擴展要求不高的服務。
相對而言,基於spring task的定時任務最簡單快捷,而xxl-job的難度主要體現在集成和調試上。無論是什么樣的定時任務,你都需要確保:
-
任務不會因為集群部署而被多次執行。
-
任務發生異常得到有效的處理
-
任務的處理過慢導致大量積壓
-
任務應該在預期的時間點執行
中間件可以將服務解耦,但增加了復雜度
