Spring Boot 2.x基礎教程:使用Elastic Job實現定時任務

程式猿DD 發表於 2021-07-22
Spring

上一篇,我們介紹瞭如何使用Spring Boot自帶的@Scheduled註解實現定時任務。文末也提及了這種方式的侷限性。當在叢集環境下的時候,如果任務的執行或操作依賴一些共享資源的話,就會存在競爭關係。如果不引入分散式鎖等機制來做排程的話,就可能出現預料之外的執行結果。所以,@Scheduled註解更偏向於使用在單例項自身維護相關的一些定時任務上會更為合理一些,比如:定時清理服務例項某個目錄下的檔案、定時上傳本例項的一些統計資料等。

那麼,在實際實現業務邏輯的時候,沒有更好的定時任務方案呢?今天我們就來介紹一個老牌的分散式定時任務框架,在Spring Boot下的使用案例。

Elasitc Job

Elastic Job的前生是噹噹開源的一款分散式任務排程框架,而目前已經加入到了Apache基金會。

該專案下有兩個分支:ElasticJob-Lite和ElasticJob-Cloud。ElasticJob-Lite是一個輕量級的任務管理方案,本文接下來的案例就用這個來實現。而
ElasticJob-Cloud則相對重一些,因為它使用容器來管理任務和隔離資源。

更多關於ElasticJob的介紹,您也可以點選這裡直達官方網站瞭解更多資訊。

動手試試

說那麼多,一起動手試試吧!

第一步:建立一個最基礎的Spring Boot專案,如果還不會?那麼看看這篇快速入門

第二步pom.xml中新增elasticjob-lite的starter

<dependencies>
    <dependency>
        <groupId>org.apache.shardingsphere.elasticjob</groupId>
        <artifactId>elasticjob-lite-spring-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>

    // ...
</dependencies>

第三步:建立一個簡單任務

@Slf4j
@Service
public class MySimpleJob implements SimpleJob {

    @Override
    public void execute(ShardingContext context) {
        log.info("MySimpleJob start : didispace.com {}", System.currentTimeMillis());
    }

}

第四步:編輯配置檔案

elasticjob.reg-center.server-lists=localhost:2181
elasticjob.reg-center.namespace=didispace

elasticjob.jobs.my-simple-job.elastic-job-class=com.didispace.chapter72.MySimpleJob
elasticjob.jobs.my-simple-job.cron=0/5 * * * * ?
elasticjob.jobs.my-simple-job.sharding-total-count=1

這裡主要有兩個部分:

第一部分:elasticjob.reg-center開頭的,主要配置elastic job的註冊中心和namespace

第二部分:任務配置,以elasticjob.jobs開頭,這裡的my-simple-job是任務的名稱,根據你的喜好命名即可,但不要重複。任務的下的配置elastic-job-class是任務的實現類,cron是執行規則表示式,sharding-total-count是任務分片的總數。我們可以通過這個引數來把任務切分,實現並行處理。這裡先設定為1,後面我們另外講分片的使用。

執行與測試

完成了上面所有操作時候,我們可以嘗試執行一下上面應用,因為這裡需要用到ZooKeeper來協調分散式環境下的任務排程。所以,你需要先在本地安裝ZooKeeper,然後啟動它。注意:上面elasticjob.reg-center.server-lists配置,根據你實際使用的ZooKeeper地址和埠做相應修改。

在啟動上述Spring Boot應用之後,我們可以看到如下日誌輸出:

2021-07-20 15:33:39.541  INFO 56365 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler 'my-simple-job' initialized from an externally provided properties instance.
2021-07-20 15:33:39.541  INFO 56365 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
2021-07-20 15:33:39.551  INFO 56365 --- [           main] org.apache.curator.utils.Compatibility   : Using org.apache.zookeeper.server.quorum.MultipleAddresses
2021-07-20 15:33:40.067  INFO 56365 --- [           main] c.d.chapter72.Chapter72Application       : Started Chapter72Application in 3.25 seconds (JVM running for 4.965)
2021-07-20 15:33:40.069  INFO 56365 --- [           main] .s.b.j.ScheduleJobBootstrapStartupRunner : Starting ElasticJob Bootstrap.
2021-07-20 15:33:40.078  INFO 56365 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler my-simple-job_$_NON_CLUSTERED started.
2021-07-20 15:33:40.078  INFO 56365 --- [           main] .s.b.j.ScheduleJobBootstrapStartupRunner : ElasticJob Bootstrap started.
2021-07-20 15:33:45.157  INFO 56365 --- [le-job_Worker-1] com.didispace.chapter72.MySimpleJob      : MySimpleJob start : didispace.com 1626766425157
2021-07-20 15:33:50.010  INFO 56365 --- [le-job_Worker-1] com.didispace.chapter72.MySimpleJob      : MySimpleJob start : didispace.com 1626766430010
2021-07-20 15:33:55.013  INFO 56365 --- [le-job_Worker-1] com.didispace.chapter72.MySimpleJob      : MySimpleJob start : didispace.com 1626766435013

既然是分散式任務排程,那麼我們再啟動一個(注意,在同一臺機器啟動的時候,會埠衝突,可以在啟動命令中加入-Dserver.port=8081來區分埠),在第二個啟動的服務日誌也列印了類似的內容

2021-07-20 15:34:06.430  INFO 56371 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler 'my-simple-job' initialized from an externally provided properties instance.
2021-07-20 15:34:06.430  INFO 56371 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
2021-07-20 15:34:06.436  INFO 56371 --- [           main] org.apache.curator.utils.Compatibility   : Using org.apache.zookeeper.server.quorum.MultipleAddresses
2021-07-20 15:34:06.786  INFO 56371 --- [           main] c.d.chapter72.Chapter72Application       : Started Chapter72Application in 1.446 seconds (JVM running for 1.884)
2021-07-20 15:34:06.787  INFO 56371 --- [           main] .s.b.j.ScheduleJobBootstrapStartupRunner : Starting ElasticJob Bootstrap.
2021-07-20 15:34:06.792  INFO 56371 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler my-simple-job_$_NON_CLUSTERED started.
2021-07-20 15:34:06.792  INFO 56371 --- [           main] .s.b.j.ScheduleJobBootstrapStartupRunner : ElasticJob Bootstrap started.
2021-07-20 15:34:10.182  INFO 56371 --- [le-job_Worker-1] com.didispace.chapter72.MySimpleJob      : MySimpleJob start : didispace.com 1626766450182
2021-07-20 15:34:15.010  INFO 56371 --- [le-job_Worker-1] com.didispace.chapter72.MySimpleJob      : MySimpleJob start : didispace.com 1626766455010
2021-07-20 15:34:20.013  INFO 56371 --- [le-job_Worker-1] com.didispace.chapter72.MySimpleJob      : MySimpleJob start : didispace.com 1626766460013

此時,在回頭看看之前第一個啟動的應用,日誌輸出停止了。由於我們設定了分片總數為1,所以這個任務啟動之後,只會有一個例項接管執行。這樣就避免了多個進行同時重複的執行相同邏輯而產生問題的情況。同時,這樣也支援了任務執行的高可用。比如:可以嘗試把第二個啟動的應用(正在列印日誌的)終止掉。可以發現,第一個啟動的應用(之前已經停止輸出日誌)繼續開始列印任務日誌了。

在整個實現過程中,我們並沒有自己手工的去編寫任何的分散式鎖等程式碼去實現任務排程邏輯,只需要關注任務邏輯本身,然後通過配置分片的方式來控制任務的分割,就可以輕鬆的實現分散式叢集環境下的定時任務管理了。是不是在複雜場景下,這種方式實現起來要比@Scheduled更方便呢?

記得自己動手寫一寫,這樣體會更深哦!如果碰到問題,可以拉取文末的程式碼示例對比一下是否有地方配置不一樣。下一篇,我們還將繼續介紹關於定時任務的一些高階內容。如果您對這個內容感興趣,可以收藏本系列教程《Spring Boot 2.x基礎教程》點選直達!。學習過程中如遇困難,可以加入我們的Spring技術交流群,參與交流與討論,更好的學習與進步!

程式碼示例

本文的完整工程可以檢視下面倉庫中的chapter7-2目錄:

如果您覺得本文不錯,歡迎Star支援,您的關注是我堅持的動力!

歡迎關注我的公眾號:程式猿DD,分享外面看不到的乾貨與思考!