Java如何實現定時任務?

我是3y,一年CRUD經驗用十年的markdown程序員👨🏻‍💻常年被譽爲優質八股文選手

挺早就規劃了要引入分佈式定時任務框架了,在年前austin就已經接入了,但代碼過年一直都沒寫,文章也就一直拖到今天了。今天主要就跟大家在聊聊定時任務這個話題。

看完這篇文章你會了解到什麼是定時任務,以及爲什麼austin項目要引入分佈式定時任務框架,可以把代碼下載下來看到我是怎麼使用xxl-job的。

01、如何簡單實現定時功能?

我是看視頻入門Java的,那時候學Java基礎API的時候,看的視頻也帶有講定時功能(JDK原生就支持),我記得視頻講師寫了Timer來講解定時任務。

當時並不知道定時任務有什麼實際作用,所以在初學階段的我,從來沒使用過Timer來實現定時的功能。

再後來,我學到併發了。那時候的講師提到了ScheduledExecutorService這個接口,它比Timer更加強大,一般我們在JDK裏可以用它來實現定時的功能

強就強在於ScheduledExecutorService內部是線程池,Timer是單線程,它能更合理的利用資源。

我學併發的時候,我也並不太關注它(它並不是併發的重點),所以我也沒用過ScheduledExecutorService來實現定時的功能。

後來吧,要到學習做項目了,那時候視頻有個Quartz課程。我記得理解了很久,最後我才反應過來了,原來寫了這麼多的代碼就是用它來實現定時的功能。

至於比ScheduledExecutorServiceTimer好在哪裏呢,最直觀的是:它支持cron表達式。

爲啥我會理解很久呢,因爲Quartzapi太複雜了(它也有着自己的專業術語和概念性的東西)。不過這種跟着做項目的,我是一步一步跟着敲代碼的。

Quartz相關的API我是記不住了,但那時候我理解了:原來我們寫代碼可以靠「組件包」來完成想要的功能,原來這就是cron表達式。

等到我大三的時候,我想用自己學過的知識點來寫個小項目,也算是梳理一遍自己到底學了什麼東西。於是,我想起了Quartz

那時候我已經學到了Spring/SpringBoot了。所以當我在網上搜SpringQuartz整合的時候,瞭解到了SpringTask,再後來發現了@Schedule註解。

只需要一個簡單的註解,就能實現定時任務的功能,並且支持cron表達式。

那那那那,還要個錘子的Quartz啊!

02、實習&&工作 定時任務

等我工作了之後,我學到了一個新的名詞「分佈式定時任務框架」。等我踏入職場了以後,我才發現原來定時任務這麼好使!

列舉下我真實工作時使用定時任務的常見姿勢:

1、動態創建定時任務推送運營類的消息(定時推送消息)

2、廣告結算定時任務掃表找到對應的可結算記錄(定時掃表更新狀態)

3、每天定時更新數據記錄(定時更新數據)

還很多人問我有沒有用過分佈式事務,我往往會回答:沒有啊,我們都是掃表一把梭保證數據最終一致性的當然了,如果是面試的時候被問到,可以吹吹分佈式事務。實際上是怎麼掃表的呢?就是定時掃的咯。

另外,我當時簡單看了下公司自研的分佈式定時任務框架是怎麼做的,我記得是基於Quartz進行擴展的,擴展有failover分片等等機制。

一般來說,使用定時任務就是在應用啓動或者提前在Web頁面配置好定時任務(定時任務框架都是支持cron表達式的,所以是週期或者定時的任務),這種場景是最最最多的。

03、爲什麼分佈式定時任務

在前面提到Timer/ScheduledExecutorService/SpringTask(@Schedule)都是單機的,但我們一旦上了生產環境,應用部署往往都是集羣模式的。

在集羣下,我們一般是希望某個定時任務只在某臺機器上執行,那這時候,單機實現的定時任務就不太好處理了。

Quartz是有集羣部署方案的,所以有的人會利用數據庫行鎖或者使用Redis分佈式鎖來自己實現定時任務跑在某一臺應用機器上;做肯定是能做的,包括有些挺出名的分佈式定時任務框架也是這樣做的,能解決問題。

但我們遇到的問題不單單隻有這些,比如我想要支持容錯功能(失敗重試)、分片功能、手動觸發一次任務、有一個比較好的管理定時任務的後臺界面路由負載均衡等等。這些功能,就是作爲「分佈式定時任務框架」所具備的。

既然現在已經有這麼多的輪子了,那我們作爲使用方/需求方就沒必要自己重新實現一套了,用現有的就好了,我們可以學習現有輪子的實現設計思想。

04、分佈式定時任務基礎

Quartz是優秀的開源組件,它將定時任務抽象了三個角色:調度器執行器任務,以至於市面上的分佈式定時任務框架都有類似角色劃分。

對於我們使用方而言,一般是引入一個client包,然後根據它的規則(可能是使用註解標識,又或是實現某個接口),隨後自定義我們自己的定時任務邏輯。

看着上面的執行圖對應的角色抽象以及一般使用姿勢,應該還是比較容易理解這個過程的。我們又可以再稍微思考兩個問題:

1、 任務信息以及調度的信息是需要存儲的,存儲在哪?調度器是需要「通知」執行器去執行的,那「通知」是以什麼方式去做?

2、調度器是怎麼找到即將需要執行的任務的呢?

針對第一個問題,分佈式定時任務框架又可以分成了兩個流派:中心化和去中心化

  • 所謂的「中心化」指的是:調度器和執行器分離,調度器統一進行調度,通知執行器去執行定時任務
  • 所謂的「去中心化」指的是:調度器和執行器耦合,自己調度自己執行

對於「中心化」流派來說,存儲相關的信息很可能是在數據庫(DataBase),而我們引入的client包實際上就是執行器相關的代碼。調度器實現了任務調度的邏輯,遠程調用執行器觸發對應的邏輯。

調度器「通知」執行器去執行任務時,可以是通過「RPC」調用,也可以是把任務信息寫入消息隊列給執行器消費來達到目的。

對於「去中心化」流派來說存儲相關的信息很可能是在註冊中心(Zookeeper),而我們引入的client包實際上就是執行器+調度器相關的代碼。

依賴註冊中心來完成任務的分配,「中心化」流派在調度的時候是需要保證一個任務只被一臺機器消費,這就需要在代碼裏寫分佈式鎖相關邏輯進行保證,而「去中心化」依賴註冊中心就免去了這個環節。

針對第二個問題,調度器是怎麼找到即將需要執行的任務的呢?現在一般較新的分佈式定時任務框架都用了「時間輪」。

1、如果我們日常要找到準備要執行的任務,可能會把這些任務放在一個List裏然後進行判斷,那此時查詢的時間複雜度爲O(n)

2、稍微改進下,我們可能把這些任務放在一個最小堆裏(對時間進行排序),那此時的增刪改時間複雜度爲O(logn),而查詢是O(1)

3、再改進下,我們把這些任務放在一個環形數組裏,那這時候的增刪改查時間複雜度都是O(1)。但此時的環形數組大小決定着我們能存放任務的大小,超出環形數組的任務就需要用另外的數組結構存放。

4、最後再改進下,我們可以有多層環形數組,不同層次的環形數組的精度是不一樣的,使用多層環形數組能大大提高我們的精度。

05、分佈式定時任務框架選型

分佈式定時任務框架現在可選擇的還是挺多的,比較出名的有:XXL-JOB/Elastic-Job/LTS/SchedulerX/Saturn/PowerJob等等等。有條件的公司可能會基於Quartz進行拓展,自研一套符合自己的公司內的分佈式定時任務框架。

我並不是做這塊出身的,對於我而言,我的austin項目技術選型主要會關注兩塊(其實跟選擇apollo作爲分佈式配置中心的理由是一樣的):成熟、穩定、社區是否活躍

這一次我選擇了xxl-job作爲austin的分佈式任務調度框架。xxl-job已經有很多公司都已經接入了(說明他的開箱即用還是很到位的)。不過最新的一個版本在2021-02,近一年沒有比較大的更新了。

06、爲什麼austin需要分佈式定時任務框架

回到austin的系統架構上,austin-admin後臺管理頁面已經被我造出來了,這個後臺管理系統會提供「消息模板」的管理功能。

那發送一條消息不單單是「技術側」調用接口進行發送的,還有很多是「運營側」通過設置定時進而推送。

而這個功能,就需要用到分佈式定時任務框架作爲中間件支撐我的業務,並且很重要的一點:分佈式定時任務框架需要支持動態創建定時任務的功能。

當在頁面點擊「啓動」的時候,就需要創建一個定時任務,當在頁面點擊「暫停」的時候,就需要停止定時任務,當在頁面點擊「刪除」模板的時候,如果曾經有過定時任務,就需要把它給一起刪掉。當在頁面點擊「編輯」並保存的時候,也需要把停止定時任務。

嗯,所需要的流程就這些了

07、austin接入xxl-job

接入xxl-job分佈式定時任務框架的步驟還是蠻簡單的(看下文檔基本就會了),我簡單說下吧。接入具體的代碼大家可以拉ausitn的下來看看,我會重點講講我接入時的感受。

官網文檔:https://www.xuxueli.com/xxl-job/#%E4%BA%8C%E3%80%81%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8

1、自己項目上引入xxl-job-core的maven依賴

2、在MySQL中執行/xxl-job/doc/db/tables_xxl_job.sql的SQL腳本

3、從GiteeGitHub下載xxl-job的源碼,修改xxl-job-admin調度中心的數據庫配置,啓動xxl-job-admin項目。

4、在自己項目上添加xxl-job相關的配置信息

5、使用@XxlJob註解修飾方法編寫定時任務的相關邏輯

從接入或者已經看過文檔的小夥伴應該就很容易發現,xxl-job它是屬於「中心化」流派的分佈式定時任務框架,調度器和執行器是分離的。

在前面我提到了austin需要動態增刪改定時任務,而xxl-job是支持的,但我覺得沒封裝得足夠好,只在調度器上給出了http接口。而調用http接口是相對麻煩的,很多相關的JavaBean都沒有在core包定義,只能我自己再寫一次。

所以,我花了挺長的時間和挺多的代碼去完成動態增刪改定時任務這個工作。

調度器和執行器是分開部署的,意味着,調度器和執行器的網絡是必須可通的:原本我在本地是沒有裝任何的環境的,包括MySQL我都是連接雲服務器的,但是現在我要調試就必須在網絡可通的環境內,所以我不得不在本地啓動xxl-job-admin調度中心來調試。

在啓動執行器的時候,會開一個新的端口給xxl-job-admin調度中心調用而不是複用SpringBoot默認端口也是挺奇怪的?

08、總結

這篇文章主要講了什麼是定時任務、爲什麼要用定時任務、在Java領域中如果有定時任務相關的需求可以用什麼來實現、分佈式定時任務的基礎知識以及如何接入XXL-JOB

相信大家對分佈式定時任務框架有了個基本的瞭解,如果感興趣可以挑個開源框架去學學,想了解接入的代碼可以把我的austin項目拉下來看看。

主要的代碼就在austin-cronxxl包下,而分佈式應用的代碼主要在austin-webMessageTemplateController跟模板的增刪改查耦合在一起了。

下一篇想來講講當定時任務被觸發,得到了一個人羣文件,我是怎麼設計去調用消息進行推送下發的。

都看到這裏了,點個贊一點都不過分吧?我是3y,下期見。

關注我的微信公衆號【Java3y】除了技術我還會聊點日常,有些話只能悄悄說~ 【對線面試官+從零編寫Java項目】 持續高強度更新中!求star!!原創不易!!求三連!!

austin項目源碼Gitee鏈接:gitee.com/austin

austin項目源碼GitHub鏈接:github.com/austin

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章