幾種主流的分布式定時任務(wù),你知道哪些?

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