幾種主流的分布式定時任務


參考: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表達式。

@Configuration@EnableSchedulingpublic class SomeJob { private static final Logger LOGGER = LoggerFactory.getLogger(SomeJob.class);
/** * 每分鍾執行一次(例:18:01:00,18:02:00) * 秒 分鍾 小時 日 月 星期 年 */ @Scheduled(cron = "0 0/1 * * * ? *") public void someTask() { //... }}
 
        

單點的定時服務在目前微服務的大環境下,應用場景越來越局限,所以嘗鮮一下分布式定時任務吧。

 

基於 Redis 實現

 

相較於之前兩種方式,這種基於Redis的實現可以通過多點來增加定時任務,多點消費。但是要做好防范重復消費的准備。

 

通過ZSet的方式

 

將定時任務存放到ZSet集合中,並且將過期時間存儲到ZSet的Score字段中,然后通過一個循環來判斷當前時間內是否有需要執行的定時任務,如果有則進行執行。

 

具體實現代碼如下:

/** * Description: 基於Redis的ZSet的定時任務 .<br> * * @author mxy * @Date 2020/8/25 11:54 */@Configuration@EnableSchedulingpublic class RedisJob { public static final String JOB_KEY = "redis.job.task"; private static final Logger LOGGER = LoggerFactory.getLogger(RedisJob.class); @Autowired private StringRedisTemplate stringRedisTemplate;
/** * 添加任務. * * @param task */ public void addTask(String task, Instant instant) { stringRedisTemplate.opsForZSet().add(JOB_KEY, task, instant.getEpochSecond()); }
/** * 定時任務隊列消費 * 每分鍾消費一次(可以縮短間隔到1s) */ @Scheduled(cron = "0 0/1 * * * ? *") public void doDelayQueue() { long nowSecond = Instant.now().getEpochSecond(); // 查詢當前時間的所有任務 Set<String> strings = stringRedisTemplate.opsForZSet().range(JOB_KEY, 0, nowSecond); for (String task : strings) { // 開始消費 task LOGGER.info("執行任務:{}", task); } // 刪除已經執行的任務 stringRedisTemplate.opsForZSet().remove(JOB_KEY, 0, nowSecond); }}
 
         
         
        

適用場景如下:

 

  • 訂單下單之后15分鍾后,用戶如果沒有付錢,系統需要自動取消訂單。

  • 紅包24小時未被查收,需要延遲執退還業務;

  • 某個活動指定在某個時間內生效&失效;

 

優勢是:

 

  1. 省去了MySQL的查詢操作,而使用性能更高的Redis做為代替;

  2. 不會因為停機等原因,遺漏要執行的任務;

 

鍵空間通知的方式

 

我們可以通過Redis的鍵空間通知來實現定時任務,它的實現思路是給所有的定時任務設置一個過期時間,等到了過期之后,我們通過訂閱過期消息就能感知到定時任務需要被執行了,此時我們執行定時任務即可。

 

默認情況下Redis是不開啟鍵空間通知的,需要我們通過config set notify-keyspace-events Ex的命令手動開啟。

 

開啟之后定時任務的代碼如下:

 

自定義監聽器
 /** * 自定義監聽器. */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 你的處理 }}

 

設置該監聽器
/** * Description: 通過訂閱Redis的過期通知來實現定時任務 .<br> * * @author mxy * @Date 2020/8/25 12:07 */@Configurationpublic 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會監聽符合以下格式的Redis消息

private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*");
 
        

基於Redis的定時任務能夠適用的場景也比較有限,但實現上相對簡單,但對於功能冪等有很大要求。從使用場景上來說,更應該叫做延時任務。

 

場景舉例:

 

  • 訂單下單之后15分鍾后,用戶如果沒有付錢,系統需要自動取消訂單。

  • 紅包24小時未被查收,需要延遲執退還業務;

 

優劣勢是:

 

  1. 被動觸發,對於服務的資源消耗更小;

  2. Redis的Pub/Sub不可靠,沒有ACK機制等,但是一般情況可以容忍;

  3. 鍵空間通知功能會耗費一些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的難度主要體現在集成和調試上。無論是什么樣的定時任務,你都需要確保:

 

  • 任務不會因為集群部署而被多次執行。

  • 任務發生異常得到有效的處理

  • 任務的處理過慢導致大量積壓

  • 任務應該在預期的時間點執行

 

中間件可以將服務解耦,但增加了復雜度


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM