Java如何實現定時任務?

Java3y發表於2022-03-25

我是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!!原創不易!!求三連!!

Java如何實現定時任務?

austin專案原始碼Gitee連結:gitee.com/austin

austin專案原始碼GitHub連結:github.com/austin

相關文章