我用定時任務實現(xiàn)了
我是3y,一年CRUD經(jīng)驗用十年的markdown程序員???????常年被譽為優(yōu)質(zhì)八股文選手
挺早就規(guī)劃了要引入分布式定時任務框架了,在年前austin就已經(jīng)接入了,但代碼過年一直都沒寫,文章也就一直拖到今天了。今天主要就跟大家在聊聊定時任務這個話題。
01、如何簡單實現(xiàn)定時功能?
我是看視頻入門Java的,那時候?qū)WJava基礎API的時候,看的視頻也帶有講定時功能(JDK原生就支持),我記得視頻講師寫了Timer來講解定時任務。

當時并不知道定時任務有什么實際作用,所以在初學階段的我,從來沒使用過Timer來實現(xiàn)定時的功能。
再后來,我學到并發(fā)了。那時候的講師提到了ScheduledExecutorService這個接口,它比Timer更加強大,一般我們在JDK里可以用它來實現(xiàn)定時的功能

強就強在于ScheduledExecutorService內(nèi)部是線程池,Timer是單線程,它能更合理的利用資源。
我學并發(fā)的時候,我也并不太關注它(它并不是并發(fā)的重點),所以我也沒用過ScheduledExecutorService來實現(xiàn)定時的功能。
后來吧,要到學習做項目了,那時候視頻有個Quartz課程。我記得理解了很久,最后我才反應過來了,原來寫了這么多的代碼就是用它來實現(xiàn)定時的功能。
至于比ScheduledExecutorService和Timer好在哪里呢,最直觀的是:它支持cron表達式。

為啥我會理解很久呢,因為Quartz的api太復雜了(它也有著自己的專業(yè)術語和概念性的東西)。這種跟著做項目的,我是一步一步跟著敲代碼的。
而Quartz相關的API我是記不住了,但那時候我理解了:原來我們寫代碼可以靠「組件包」來完成想要的功能,原來這就是cron表達式。
等到我大三的時候,我想用自己學過的知識點來寫個小項目,也算是梳理一遍自己到底學了什么東西。于是,我想起了Quartz。
那時候我也已經(jīng)學到了Spring/SpringBoot了,所以當我在網(wǎng)上搜Spring與Quartz整合的時候,了解到了SpringTask,再后來發(fā)現(xiàn)了@Schedule注解。

只需要一個簡單的注解,就能實現(xiàn)定時任務的功能,并且支持cron表達式。
那那那那,還要個錘子的Quartz啊!
02、實習&&工作 ?定時任務
等我工作了之后,我學到了一個新的名詞「分布式定時任務框架」。等我踏入職場了以后,我才發(fā)現(xiàn)原來定時任務這么好使!
列舉下我真實工作時使用定時任務的常見姿勢:
1、動態(tài)創(chuàng)建定時任務推送運營類的消息(定時推送消息)
2、廣告結(jié)算定時任務掃表找到對應的可結(jié)算記錄(定時掃表更新狀態(tài))
3、每天定時更新數(shù)據(jù)記錄(定時更新數(shù)據(jù))
還很多人問我有沒有用過分布式事務,我往往會回答:沒有啊,我們都是掃表一把梭保證數(shù)據(jù)最終一致性的。當然了,如果是面試的時候被問到,可以吹吹分布式事務。實際上是怎么掃表的呢?就是定時掃的咯。
另外,我當時簡單看了下公司自研的分布式定時任務框架是怎么做的,我記得是基于Quartz進行擴展的,擴展有failover、分片等等機制。
一般來說,使用定時任務就是在應用啟動或者提前在Web頁面配置好定時任務(定時任務框架都是支持cron表達式的,所以是周期或者定時的任務),這種場景是最最最多的。

03、為什么分布式定時任務
在前面提到Timer/ScheduledExecutorService/SpringTask(@Schedule)都是單機的,但我們一旦上了生產(chǎn)環(huán)境,應用部署往往都是集群模式的。
在集群下,我們一般是希望某個定時任務只在某臺機器上執(zhí)行,那這時候,單機實現(xiàn)的定時任務就不太好處理了。
Quartz是有集群部署方案的,所以有的人會利用數(shù)據(jù)庫行鎖或者使用Redis分布式鎖來自己實現(xiàn)定時任務跑在某一臺應用機器上;做肯定是能做的,包括有些挺出名的分布式定時任務框架也是這樣做的,能解決問題。
但我們遇到的問題不單單只有這些,比如我想要支持容錯功能(失敗重試)、分片功能、手動觸發(fā)一次任務、有一個比較好的管理定時任務的后臺界面、路由負載均衡等等。這些功能,就是作為「分布式定時任務框架」所具備的。
既然現(xiàn)在已經(jīng)有這么多的輪子了,那我們作為使用方/需求方就沒必要自己重新實現(xiàn)一套了,用現(xiàn)有的就好了,我們可以學習現(xiàn)有輪子的實現(xiàn)設計思想。
04、分布式定時任務基礎
Quartz是優(yōu)秀的開源組件,它將定時任務抽象了三個角色:調(diào)度器、執(zhí)行器和任務,以至于市面上的分布式定時任務框架都有類似角色劃分。

對于我們使用方而言,一般是引入一個client包,然后根據(jù)它的規(guī)則(可能是使用注解標識,又或是實現(xiàn)某個接口),隨后自定義我們自己的定時任務邏輯。

看著上面的執(zhí)行圖對應的角色抽象以及一般使用姿勢,應該還是比較容易理解這個過程的。我們又可以再稍微思考兩個問題:
1、 任務信息以及調(diào)度的信息是需要存儲的,存儲在哪?調(diào)度器是需要「通知」執(zhí)行器去執(zhí)行的,那「通知」是以什么方式去做?
2、調(diào)度器是怎么找到即將需要執(zhí)行的任務的呢?
針對第一個問題,分布式定時任務框架又可以分成了兩個流派:中心化和去中心化
所謂的「中心化」指的是:調(diào)度器和執(zhí)行器分離,調(diào)度器統(tǒng)一進行調(diào)度,通知執(zhí)行器去執(zhí)行定時任務 所謂的「去中心化」指的是:調(diào)度器和執(zhí)行器耦合,自己調(diào)度自己執(zhí)行
對于「中心化」流派來說,存儲相關的信息很可能是在數(shù)據(jù)庫(DataBase),而我們引入的client包實際上就是執(zhí)行器相關的代碼。調(diào)度器實現(xiàn)了任務調(diào)度的邏輯,遠程調(diào)用執(zhí)行器觸發(fā)對應的邏輯。

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

對于「去中心化」流派來說存儲相關的信息很可能是在注冊中心(Zookeeper),而我們引入的client包實際上就是執(zhí)行器+調(diào)度器相關的代碼。
依賴注冊中心來完成任務的分配,「中心化」流派在調(diào)度的時候是需要保證一個任務只被一臺機器消費,這就需要在代碼里寫分布式鎖相關邏輯進行保證,而「去中心化」依賴注冊中心就免去了這個環(huán)節(jié)。

針對第二個問題,調(diào)度器是怎么找到即將需要執(zhí)行的任務的呢?現(xiàn)在一般較新的分布式定時任務框架都用了「時間輪」。
1、如果我們?nèi)粘R业綔蕚湟獔?zhí)行的任務,可能會把這些任務放在一個List里然后進行判斷,那此時查詢的時間復雜度為O(n)
2、稍微改進下,我們可能把這些任務放在一個最小堆里(對時間進行排序),那此時的增刪改時間復雜度為O(logn),而查詢是O(1)
3、再改進下,我們把這些任務放在一個環(huán)形數(shù)組里,那這時候的增刪改查時間復雜度都是O(1)。但此時的環(huán)形數(shù)組大小決定著我們能存放任務的大小,超出環(huán)形數(shù)組的任務就需要用另外的數(shù)組結(jié)構(gòu)存放。
4、最后再改進下,我們可以有多層環(huán)形數(shù)組,不同層次的環(huán)形數(shù)組的精度是不一樣的,使用多層環(huán)形數(shù)組能大大提高我們的精度。

05、分布式定時任務框架選型
分布式定時任務框架現(xiàn)在可選擇的還是挺多的,比較出名的有:XXL-JOB/Elastic-Job/LTS/SchedulerX/Saturn/PowerJob等等等。有條件的公司可能會基于Quartz進行拓展,自研一套符合自己的公司內(nèi)的分布式定時任務框架。
我并不是做這塊出身的,對于我而言,我的austin項目技術選型主要會關注兩塊(其實跟選擇apollo作為分布式配置中心的理由是一樣的):成熟、穩(wěn)定、社區(qū)是否活躍。
這一次我選擇了xxl-job作為austin的分布式任務調(diào)度框架。xxl-job已經(jīng)有很多公司都已經(jīng)接入了(說明他的開箱即用還是很到位的)。不過最新的一個版本在2021-02,近一年沒有比較大的更新了。

06、為什么austin需要分布式定時任務框架
回到austin的系統(tǒng)架構(gòu)上,austin-admin后臺管理頁面已經(jīng)被我造出來了,這個后臺管理系統(tǒng)會提供「消息模板」的管理功能。

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


而這個功能,就需要用到分布式定時任務框架作為中間件支撐我的業(yè)務,并且很重要的一點:分布式定時任務框架需要支持動態(tài)創(chuàng)建定時任務的功能。
當在頁面點擊「啟動」的時候,就需要創(chuàng)建一個定時任務,當在頁面點擊「暫停」的時候,就需要停止定時任務,當在頁面點擊「刪除」模板的時候,如果曾經(jīng)有過定時任務,就需要把它給一起刪掉。當在頁面點擊「編輯」并保存的時候,也需要把停止定時任務。
嗯,所需要的流程就這些了
07、austin接入xxl-job
接入xxl-job分布式定時任務框架的步驟還是蠻簡單的(看下文檔基本就會了),我簡單說下吧。接入具體的代碼大家可以拉ausitn的下來看看,我會重點講講我接入時的感受。
官網(wǎng)文檔: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中執(zhí)行/xxl-job/doc/db/tables_xxl_job.sql的SQL腳本
3、從Gitee或GitHub下載xxl-job的源碼,修改xxl-job-admin調(diào)度中心的數(shù)據(jù)庫配置,啟動xxl-job-admin項目。
4、在自己項目上添加xxl-job相關的配置信息
5、使用@XxlJob注解修飾方法編寫定時任務的相關邏輯

從接入或者已經(jīng)看過文檔的小伙伴應該就很容易發(fā)現(xiàn),xxl-job它是屬于「中心化」流派的分布式定時任務框架,調(diào)度器和執(zhí)行器是分離的。

在前面我提到了austin需要動態(tài)增刪改定時任務,而xxl-job是支持的,但我覺得沒封裝得足夠好,只在調(diào)度器上給出了http接口。而調(diào)用http接口是相對麻煩的,很多相關的JavaBean都沒有在core包定義,只能我自己再寫一次。
所以,我花了挺長的時間和挺多的代碼去完成動態(tài)增刪改定時任務這個工作。

調(diào)度器和執(zhí)行器是分開部署的,意味著,調(diào)度器和執(zhí)行器的網(wǎng)絡是必須可通的:原本我在本地是沒有裝任何的環(huán)境的,包括MySQL我都是連接云服務器的,但是現(xiàn)在我要調(diào)試就必須在網(wǎng)絡可通的環(huán)境內(nèi),所以我不得不在本地啟動xxl-job-admin調(diào)度中心來調(diào)試。
在啟動執(zhí)行器的時候,會開一個新的端口給xxl-job-admin調(diào)度中心調(diào)用而不是復用SpringBoot默認端口也是挺奇怪的?

08、總結(jié)
這篇文章主要講了什么是定時任務、為什么要用定時任務、在Java領域中如果有定時任務相關的需求可以用什么來實現(xiàn)、分布式定時任務的基礎知識以及如何接入XXL-JOB
相信大家對分布式定時任務框架有了個基本的了解,如果感興趣可以挑個開源框架去學學,想了解接入的代碼可以把我的austin項目拉下來看看。
主要的代碼就在austin-cron的xxl包下,而分布式應用的代碼主要在austin-web的MessageTemplateController跟模板的增刪改查耦合在一起了。
下一篇想來講講當定時任務被觸發(fā),得到了一個人群文件,我是怎么設計去調(diào)用消息進行推送下發(fā)的。
《對線面試官》公眾號還在持續(xù)分享面試題,沒關注的同學可以關注一波!這是austin項目的上一個系列,質(zhì)量桿桿的
austin項目Gitee鏈接:https://gitee.com/zhongfucheng/austin
austin項目GitHub鏈接:https://github.com/ZhongFuCheng3y/austin
閱讀原文可跳轉(zhuǎn)austin倉庫
