Spring Boot 2.x 集成 Quartz 定時器 jdbc 持久化、配置集羣

目錄

JDBC JobStore 持久化步驟概述

Spring Boot  集成 Quartz 定時器

Scheduer 調度器常用方法

JobDetal 與 Trigger 一對多

Quartz Scheduler 配置集羣


1、本文環境:Spring boot 2.1.3 + quartz 2.3.0 + Mysql 驅動 8.0.15 + H2 驅動 1.4 + Java JDK 1.8。

(支持 mysql 數據庫與 h2 數據庫,如果沒有安裝 mysql 的,可以切換嵌入式的 h2 數據庫)

本文源碼:https://github.com/wangmaoxiong/quartzjdbc

JDBC JobStore 持久化步驟概述

1、《Quartz 數據持久化》默認使用 org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore 內存存儲,本文介紹 JobStoreTX jdbc 存儲。

2、Quartz 調度信息可以通過 JDBC 保存到數據庫中,支持主流關係型數據庫,如:Oracle,PostgreSQL,MySQL,H2,SQL Server,HSQLDB 和 DB2 等。

3、實際生產中通常都是使用 JDBC 持久化,配置的調度信息都存儲在數據中,應用服務器重啓之後自動讀取調度信息,然後繼續按着規則進行自動執行,就像是一塊鐘錶即使沒電了,下次上了電池之後,仍然會自動運行。

一:創建數據庫表

1、要使用 JDBC 持久化數據,首先必須創建一組數據庫表以供 Quartz 使用,針對不同的數據庫,org.quartz.impl.jdbcjobstore 包下提供了不同建表腳本。所有的表前綴都是 "QRTZ_",如 "QRTZ_TRIGGERS"、"QRTZ_JOB_DETAIL"。

2、執行程序之前,必須先手動執行腳本建表,否則啓動時會報表不存在。而對於 h2 這種內存數據庫,如果沒有手動建表,則它會自動建表。

3、建表腳本傳送,總共11張表,分別如下:

序號 表名 描述
1 qrtz_fired_triggers 存儲與已觸發的 Trigger 相關的狀態信息,以及相聯 Job 的執行信息
2

qrtz_paused_trigger_grps

存儲已暫停的 Trigger 相關信息
3 qrtz_scheduler_state 存儲有關 Scheduler 的狀態信息
4 qrtz_locks 存儲程序鎖信息
5 qrtz_simple_triggers 存儲配置的 Simple Trigger 觸發器信息
6 qrtz_simprop_triggers 存儲即興觸發器信息
7 qrtz_cron_triggers 存儲 Cron Trigger 觸發器信息,如 Cron 表達式
8 qrtz_blob_triggers  
9 qrtz_triggers 存儲已配置的 Trigger 的信息
10 qrtz_job_details 存儲已配置的 Job 的詳細信息
11 qrtz_calendars 存儲 Quartz 的 Calendar 信息

二:確定事務管理類型

1、JobStoreTX:如果不需要將調度命令(例如添加和刪除triggers)綁定到其他事務,那麼可以通過使用 JobStoreTX 管理事務(這是最常見的選擇)。

2、JobStoreCMT:如果需要 Quartz 與其他事務(即J2EE應用程序服務器)一起工作,那麼應該使用 JobStoreCMT,這種情況下,Quartz 將讓應用程序服務器容器管理事務。

#需要使用哪一種事務類型,配置文件中就指定誰
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreCMT

三:設置數據源

1、quartz 的 JDBC 持久化需要從 DataSource 中獲取與數據庫的連接,DataSources 可以通過三種方式進行配置:

1)在 quartz 自己的配置文件 quartz.properties 中指定的所有池屬性,以便 Quartz 可以自己創建 DataSource。w3c 教程官網配置.
2)Quartz 直接使用應用服務器配置好的數據源。(本文使用方式)
3) 自定義的 org.quartz.utils.ConnectionProvider 實現

四:確定數據庫驅動代理

1、需要爲 JobStore 選擇一個 DriverDelegate 才能使用,驅動代理負責執行特定數據庫可能需要的任何 JDBC 工作

2、針對不同的數據庫製作了不同的數據庫的代理,其中使用最多的是 StdJDBCDelegate 是一個使用 JDBC 代碼(和SQL語句)來執行其工作的委託。其他驅動代理可以在 "org.quartz.impl.jdbcjobstore" 包或其子包中找到。如 DB2v6Delegate(用於DB2版本6及更早版本),HSQLDBDelegate(用於HSQLDB),MSSQLDelegate(SQLServer),PostgreSQLDelegate(用於PostgreSQL)),WeblogicDelegate(用於使用Weblogic創建的JDBC驅動程序)

3、配置文件中配置如下:

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

4、quartz.properties 可用屬性的完整配置信息可以參考官網 Quartz Configuration,但也不是完全能用到那麼多,下面將通過實際案例來進行一一使用。

Spring Boot  集成 Quartz 定時器

1、通過封裝公共的調度器業務層和控制層,以後需要新加功能時,則只需要新增 Job 實現即可,實現功能如下:

1)http://localhost:8080/schedule/findSchedulers?pageNum=1&pageSize=10:查詢註冊成功的作業信息
2)http://localhost:8080/schedule/scheduleJob:註冊作業並啓動
3)http://localhost:8080/schedule/rescheduleJob:重新註冊任務的觸發器
4)http://localhost:8080/schedule/deleteJob?jobName=xx&jobGroup=:刪除指定調度作業
5)http://localhost:8080/schedule/pauseJob?jobName=xx&jobGroup=:暫停指定作業
6)http://localhost:8080/schedule/pauseAll:暫停所有作業
7)http://localhost:8080/schedule/resumeJob?jobName=xx&jobGroup=:恢復指定作業繼續運行
8)http://localhost:8080/schedule/resumeAll:恢復所有作業
...

2、https://github.com/wangmaoxiong/quartzjdbc 源碼中都有詳細註釋,所以爲了不重複贅述,下面只提醒注意事項。

一:數據庫建表

1、上面已經提到:執行程序之前,必須先手動執行腳本建表,否則啓動時會報錯表不存在。而對於 h2 這種內存數據庫,如果沒有手動建表,則它會自動建表。

2、建表腳本。表的含義查看上面。

二:pom.xml 依賴https://github.com/wangmaoxiong/quartzjdbc/blob/master/pom.xml

1、導入了 mysql 數據庫驅動和 h2 數據庫驅動,如果沒有安裝 mysql 的,可以在配置文件中切換嵌入式的 h2 數據庫.

2、spring-boot-starter-quartz 組件內部依賴瞭如下的組件

ategory/License Group / Artifact Version
Job Scheduling/Apache 2.0 org.quartz-scheduler » quartz 2.3.2
Apache 2.0 org.springframework » spring-context-support 5.2.4.RELEASE
Transactions/Apache 2.0 org.springframework » spring-tx 5.2.4.RELEASE
Apache 2.0 org.springframework.boot » spring-boot-starter 2.2.5.RELEASE

三:全局配置文件:https://github.com/wangmaoxiong/quartzjdbc/blob/master/src/main/resources/application.yml

1、如果沒有安裝 mysql 的,可以切換嵌入式的 h2 數據庫:spring.profiles.active=h2DB

2、Spring Boot 對 Quartz Scheduler 集成之後,對它提供了屬性配置,選項如下:

spring.quartz.auto-startup=true # 初始化後是否自動啓動計劃程序
spring.quartz.jdbc.comment-prefix=-- # SQL 初始化腳本中單行註釋的前綴
spring.quartz.jdbc.initialize-schema=embedded # 數據庫架構初始化模式
# 用於初始化數據庫架構的SQL文件的路徑
spring.quartz.jdbc.schema=classpath:org/quartz/impl/jdbcjobstore/tables_@@platform@@.sql 
spring.quartz.job-store-type=memory # 石英調度器作業/任務存儲類型
spring.quartz.overwrite-existing-jobs=false # 配置的作業是否應覆蓋現有的作業定義
spring.quartz.properties.*= # 其他石英調度器屬性,值是一個 Map
spring.quartz.scheduler-name=quartzScheduler # 計劃程序的名稱
spring.quartz.startup-delay=0s # 初始化完成後啓動計劃程序的延遲時間
spring.quartz.wait-for-jobs-to-complete-on-shutdown=false # 關閉時是否等待正在運行的作業完成

Spring boot 2.1.6 文檔官網:common-application-properties 中可以查看到這些配置信息.

3、org.springframework.boot.autoconfigure.quartz.QuartzProperties 類專門映射 spring.quartz 開頭的屬性.

4、quartz 調度器自己的配置屬性既可以配置在自己的 quartz.properties 配置文件中,也可以直接配置在 Spring boot 的 spring.quartz.properties 屬性下,它是一個 Map<String, String> 類型。

5、quartz 的所有配置都可以從官網 Quartz Configuration 獲取.

四:BeanConfig 配置類:config/BeanConfig.java

1、Spring boot 源碼 org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration 中可以看到應用啓動時已經自動創建了 SchedulerFactoryBean 實例,所以需要注入它,然後 schedulerFactoryBean.getScheduler() 獲取 Schduler。:

@Bean
@ConditionalOnMissingBean
public SchedulerFactoryBean quartzScheduler() {
	SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
...

2、爲了後期獲取 Schduler,這裏提供配置類將它提交給容器管理。同時提供了 RestTemplate 模板,方便後期做 http 請求.

五:頁面返回值實體

1、ResultData 封裝返回給頁面的數據,由 code、message、data 三個屬性組成,ResultCode 枚舉定義常見的返回值狀態.

java/com/wmx/quartzjdbc/pojo/ResultData.java
java/com/wmx/quartzjdbc/enums/ResultCode.java

六:調度信息實體:pojo/SchedulerEntity.java

1、爲了方便註冊調度任務與觸發器,於是從 qrtz_job_details、qrtz_triggers 表中提取了一些常用字段出來組成實體,方便麪向對象編程。

2、一是爲了編碼簡單、二是考慮到 cron 觸發器基本能滿足開發需求,所以只操作 cron 觸發器,其它觸發器開發同理。

job_class_name:作業詳情關聯的 Job 類名,必須關聯正確。
ron_expression: cron 觸發器表達式,格式必須正確。在線Cron表達式生成器

七:quartz 作業/任務:jobs/RequestJob.java

1、執行定時任務邏輯的類。爲了方便注入其它 service,或者其它組件,所以將類標識爲 @Service 組件。

2、《Job/JobDetail 實例 與 併發》中說過 @DisallowConcurrentExecution、@PersistJobDataAfterExecution 註解用於處理高併發情況,當同一個 JobDetail 實例被併發執行時,由於競爭,JobDataMap 中存儲的數據很可能是不確定的。

3、項目中以後新增任務功能時,只需要再實現 Job 接口編寫業務邏輯即可。

八:調度業務層:service/SchedulerService.java

1、專門封裝了 "啓動、暫停、恢復、刪除作業(Job)或者觸發器",封裝之後就通用了,所有的任務註冊、暫停、刪除等操作,都可以傳入參數調用本類。以後如果在需要添加業務需求時,則只需要關心實現 Job 即可。

2、因爲事先已經提供了 Scheduler ,所以現在只需要使用 @Resource 注入即可。

3、註冊作業的時候注意下面兩個參數:

3.1)storeDurably(boolean jobDurability):指示 job 是否是持久性的。如果 job 是非持久的,當沒有活躍的 trigger 與之關聯時,就會被自動地從 scheduler 中刪除。即非持久的 job 的生命期是由 trigger 的存在與否決定的.
3.2)requestRecovery(boolean jobShouldRecover) :指示  job 遇到故障重啓後,是否是可恢復的。如果 job 是可恢復的,在其執行的時候,如果 scheduler 發生硬關閉(hard shutdown)(比如運行的進程崩潰了,或者關機了),則當 scheduler 重啓時,該 job 會被重新執行。

4、因爲全局配置文件中配置了 spring.quartz.uto-startup=true,所以代碼中不需要再手動啓動:scheduler.start();

九:調度控制層:controller/SchedulerController.java

1、對外提供訪問接口,通過約定參數進行註冊任務、暫停、刪除等操作。

2、http://localhost:8080/schedule/scheduleJob:註冊作業並啓動,post 提交的參數舉例如下:

{
  "job_name": "j2000",
  "job_group": "reqJobGroup",
  "job_class_name": "com.wmx.quartzjdbc.jobs.RequestJob",
  "job_data": {
  "url": "https://wangmaoxiong.blog.csdn.net/article/details/105080021"
  },
  "trigger_name": "t2000",
  "trigger_group": "requestGroup",
  "trigger_desc": "每1分鐘訪問一次",
  "cron_expression": "0 0/1 * * * ?"
}

3、http://localhost:8080/schedule/rescheduleJob:重新註冊任務的觸發器,post 提交的參數舉例如下:

{
  "trigger_name": "t2000",
  "trigger_group": "requestGroup",
  "trigger_desc": "每1分鐘訪問一次",
  "cron_expression": "0 0/1 * * * ?",
  "trigger_data": {
  "url": "https://wangmaoxiong.blog.csdn.net/article/details/105057405"
  }
}

其它接口都親測有效,可以自行測試。

Scheduer 調度器常用方法

1、綜上所述,整個調度器關鍵的就是 Scheduler 對象,所以它的常用 API 方法彙總如下:

方法  描述
addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers) 向調度程序註冊給定"日曆",如果使用 jdbc 持久化,則對應 qrtz_calendars 表.
1)calName:假期日曆的名稱,觸發器會根據名稱進行引用、calendar:假期日曆
2)replace:表示調度器 scheduler 中如果已經存在同名的日曆是否替換
3)updateTriggers:是否更新引用了現有日曆的現有觸發器,以使其基於新觸發器是"正確的".
addJob(JobDetail jobDetail, boolean replace) 將給定的作業添加到調度程序,此時未關聯觸發器,作業將"休眠"到它使用 Scheduler.triggerJob、或者 scheduler.scheduleJob 進行調度。
1)作業必須定義爲"持久"(durability=true),即使沒有關聯觸發器時,也不會被自動刪除。
2)如果存在內部計劃程序錯誤,或者如果作業不是持久,或已存在同名作業且 replace=false,都會拋出 拋出SchedulerException
addJob(JobDetail jobDetail, boolean replace, boolean storeNonDurableWhileAwaitingScheduling)

storenondurablewileawitingscheduling 設置爲 true,則可以存儲非持久作業,一旦 Job 按計劃開始執行,它將恢復正常的非持久行爲(即一旦關聯觸發器的都結束時就會被刪除)。

boolean checkExists(JobKey jobKey) 根據 jobKey 檢查對應的 Job 是否已經存在於計劃程序中。如果存在具有給定標識符的作業,則返回 true。
boolean checkExists(TriggerKey triggerKey) 根據 triggerKey 檢查對應的 Trigger 是否已經存在於計劃程序中。如果存在具有給定標識符的觸發器,則返回 true。
clear() 清除/刪除所有計劃數據,包括所有的 Job,所有的 Trigger,所有的 日曆。如果 jdbc 持久化,則 clear 後,數據庫相應表中的數據全部會被刪除。可以查看源碼:org.quartz.impl.jdbcjobstore.StdJDBCDelegate#clearData
deleteCalendar(String calName) 根據名稱從調度程序中刪除指定的日曆。如果找到並刪除了日曆,則返回 true。
如果存在內部調度程序錯誤,或一個或多個觸發器引用了被刪除的日曆,則拋 SchedulerException。
boolean deleteJob(JobKey jobKey) 根據 JokKey 從調度程序中刪除指定的作業及其關聯的觸發器。
如果找到並刪除作業,則返回 true,如果存在內部計劃程序錯誤,則拋出SchedulerException
deleteJobs(List<JobKey> jobKeys) 批量刪除作業及其關聯的觸發器。如果找到並刪除了所有作業,則返回 true;如果一個或多個未刪除,則返回 false;
Calendar getCalendar(String calName) 根據名稱獲取調度器中註冊好的日曆
List<String> getCalendarNames() 獲取所有註冊了的 Calendar 名稱
SchedulerContext getContext() 返回調度程序的SchedulerContext。
List<JobExecutionContext> getCurrentlyExecutingJobs() 返回此計劃程序實例中當前正在執行的所有作業。此方法不支持羣集,只返回當前工作崗位,而不是在整個集羣。
注意返回的列表是一個"瞬時"快照,並且一旦返回,執行作業的真正列表可能會有所不同。
JobDetail getJobDetail(JobKey jobKey) 根據 jobKet 獲取作業詳情 JobDetail,返回的 JobDetail 對象是實際存儲的快照。
如果要修改作業詳細信息,則必須重新存儲,如 addJob(JobDetail,boolean)。
List<String> getJobGroupNames() 獲取所有已知的 jobdail 組名稱.
Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher) 使用 GroupMatcher 獲取匹配的 jobKey 。
Set<String> getPausedTriggerGroups() 獲取所有暫停的 Trigger 組的名稱。
String getSchedulerName() 返回調度程序的名稱
String getSchedulerInstanceId() 返回調度程序的實例Id
Trigger getTrigger(TriggerKey triggerKey) 使用給定的鍵獲取 Trigger 實例,返回的觸發器對象是實際存儲的。如果要修改觸發器,必須重新存儲之後再觸發(如 rescheduleejob(TriggerKey,trigger)。
List<String> getTriggerGroupNames() 獲取所有已知的 Trigger 組的名稱.
Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher) 根據 GroupMatcher 獲取匹配的觸發器集合.
List<? extends Trigger> getTriggersOfJob(JobKey jobKey) 根據 jobKey 獲取其關聯的觸發器列表。因爲job與trigger是一對多.
TriggerState getTriggerState(TriggerKey triggerKey) 觸發器的狀態,如:PAUSED(暫停)、ACQUIRED(活動)、WAITING(等待)
pauseAll() 暫停所有觸發器. 新觸發器將在添加時暫停。
pauseJob(JobKey jobKey) 暫停指定作業下的所有觸發器.
pauseJobs(GroupMatcher<JobKey> matcher) 暫停 GroupMatcher 匹配到的所有作業下的所有觸發器
pauseTrigger(TriggerKey triggerKey) 暫停指定的觸發器.
pauseTriggers(GroupMatcher<TriggerKey> matcher) 暫停 GroupMatcher 匹配到的多個觸發器.
Date rescheduleJob(TriggerKey triggerKey, Trigger newTrigger) 重新註冊觸發器。先根據 triggerKey 刪除指定的觸發器,然後存儲新觸發器(newTrigger),並關聯相同的作業.
resumeAll() 恢復(取消暫停)所有觸發器。如果有觸發器因爲暫停錯過一次或多次觸發,則過期執行策略將被應用。
resumeJob(JobKey jobKey) 恢復(取消暫停)指定作業下的所有觸發器.
resumeJobs(GroupMatcher<JobKey> matcher) 批量恢復(取消暫停)作業.
resumeTrigger(TriggerKey triggerKey) 恢復(取消暫停)指定觸發器
resumeTriggers(GroupMatcher<TriggerKey> matcher) 批量恢復(取消暫停)觸發器.
Date scheduleJob(Trigger trigger) 註冊/調度給定的 Trigger,注意觸發器必須正確關聯已經存在的作業(Job),如使用 forJob(JobDetail jobDetail)
如果 trigger 未關聯作業,或者 job 不存在,則拋 SchedulerException.
如果觸發器不能添加到計劃程序,或者有內部錯誤,則拋 SchedulerException. 如果註冊的觸發器已經存在,則拋異常,此時建議使用 rescheduleJob 修改觸發器.
Date scheduleJob(JobDetail jobDetail, Trigger trigger) 將給定的 jobdail 註冊/添加到調度程序,同時將給定的 Trigger 與之關聯.
1)如果 trigger 已經引用(forJob(JobDetail jobDetail))了 jobDetail 之外的作業,則拋 SchedulerException.
2)如果 jobDetail、trigger 已經存在同名的 group 與 name,則也會拋異常.
scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace) 單個作業關聯多個觸發器註冊
replace:如果註冊的 jobDetail、trigger 已經存在,則更新它們.
scheduleJobs(Map<JobDetail, Set<? extends Trigger>> triggersAndJobs, boolean replace 批量註冊 JobDetail 與 triggersAndJobs,即一個作業對應多個觸發器.
replace:表示如果當存在同組同名的作業或者觸發器時,是否更新它們。如果爲 false,則必須保證添加的作業和觸發器唯一,否則拋異常.
shutdown() 停止/關閉 quartz 調度程序,關閉了整個調度的線程池,意味者所有作業都不會繼續執行。相當於 shutdown(false)。關閉後無法重新啓動計劃程序,只能重啓應用
shutdown(boolean waitForJobsToComplete) 參數表示是否如果當時作業正在執行,是否等待它執行完畢。關閉後無法重新啓動計劃程序,只能重啓應用
start() 啓動觸發 Trigger 的調度程序的線程,如果是首次呼叫,將啓動失火/恢復過程。
如果 start 前已經調用了shutdown() 方法則拋出 SchedulerException.
startDelayed(int seconds) 延遲 seconds 秒後再啓動,此方法不阻塞.
boolean unscheduleJob(TriggerKey triggerKey) 從調度程序中刪除指定的 Trigger,如果相關關聯的作業沒有任何其他觸發器,並且該作業是不持久的,則作業也將被刪除。
boolean unscheduleJobs(List<TriggerKey> triggerKeys) 批量刪除所有指定的觸發器.
isShutdown() 報告 調度程序 是否已關閉。

JobDetal 與 Trigger 一對多

1、quartz 設計的 Job、Trigger、Calendar 是相互獨立的

2、Job 與 trigger 是一對多:qrtz_job_details 表中有外鍵關聯 qrtz_job_details 表中的 sched_name, job_name, job_group。

3、Job 被創建後,可以保存在 Scheduler 中,與 Trigger 是獨立的,一個 Job 可以有多個 Trigger;這種松耦合的一個好處是可以修改或者替換 Trigger,而不用重新定義與之關聯的 Job。

4、Calendar 通過 addCalendar 方法註冊到 scheduler,觸發器再通過 modifiedByCalendar(String calendarName)關聯日曆,同一個 Calendar 實例可用於多個 trigger

5、主要是下面三個方法:

addJob(JobDetail jobDetail, boolean replace):註冊作業.
scheduleJob(Trigger trigger):註冊觸發器,觸發器中使用 triggerBuilder.forJob 方法先關聯作業.
rescheduleJob(TriggerKey triggerKey, Trigger newTrigger):修改觸發器,使用新觸發器更新已存在的舊觸發器.

    /**
     * 註冊 job 與 觸發器。區別於上面的是這裏會對 作業和觸發器進行分開註冊.
     * job_class_name 不能爲空時,註冊 JobDetail 作業詳情,如果已經存在,則更新.
     * cron_expression 不爲空時,註冊觸發器(註冊觸發器時,對應的作業必須先存在):
     * <span>根據參數 job_name、job_group 獲取 JobDetail,如果存在,則關聯此觸發器與 JobDetail,然後註冊觸發器,</span>
     *
     * @param schedulerEntity
     * @return
     */
    @PostMapping("schedule/scheduleJobOrTrigger")
    public ResultData scheduleJobOrTrigger(@RequestBody SchedulerEntity schedulerEntity) {
        ResultData resultData = null;
        try {
            schedulerService.scheduleJobOrTrigger(schedulerEntity);
            resultData = new ResultData(ResultCode.SUCCESS, null);
        } catch (Exception e) {
            resultData = new ResultData(ResultCode.FAIL, null);
            logger.error(e.getMessage(), e);
        }
        return resultData;
    }

業務層實現如下:

    /**
     * 註冊 job 與 觸發器。區別於上面的是這裏會對 作業和觸發器進行分開註冊.
     * job_class_name 不能爲空時,註冊 JobDetail 作業詳情,如果已經存在,則更新,不存在,則添加.
     * cron_expression 不爲空時,註冊觸發器(註冊觸發器時,對應的作業必須先存在):
     * <span>根據參數 job_name、job_group 獲取 JobDetail,如果存在,則關聯此觸發器與 JobDetail,然後註冊觸發器,</span>
     *
     * @param schedulerEntity
     * @throws SchedulerException
     */
    public void scheduleJobOrTrigger(SchedulerEntity schedulerEntity) throws SchedulerException, ClassNotFoundException {
        //1)註冊 job 作業
        String job_class_name = schedulerEntity.getJob_class_name();
        JobDetail jobDetail = null;
        if (StringUtils.isNotBlank(job_class_name)) {
            jobDetail = this.getJobDetail(schedulerEntity);
            //往調度器中添加作業.
            scheduler.addJob(jobDetail, true);
            logger.info("往調度器中添加作業 {}," + jobDetail.getKey());
        }
        //2)註冊觸發器,觸發器必須關聯已經存在的作業
        String job_name = schedulerEntity.getJob_name();
        String job_group = schedulerEntity.getJob_group();
        if (jobDetail == null && StringUtils.isNotBlank(job_group) && StringUtils.isNotBlank(job_name)) {
            jobDetail = scheduler.getJobDetail(JobKey.jobKey(job_name, job_group));
        }
        String cron_expression = schedulerEntity.getCron_expression();
        Trigger trigger = null;
        if (jobDetail != null && StringUtils.isNotBlank(cron_expression)) {
            trigger = this.getTrigger(schedulerEntity, JobKey.jobKey(job_name, job_group));
        }
        if (trigger == null) {
            return;
        }
        //註冊觸發器。如果觸發器不存在,則新增,否則修改
        boolean checkExists = scheduler.checkExists(trigger.getKey());
        if (checkExists) {
            //rescheduleJob(TriggerKey triggerKey, Trigger newTrigger):更新指定的觸發器.
            scheduler.rescheduleJob(trigger.getKey(), trigger);
        } else {
            //scheduleJob(Trigger trigger):註冊觸發器,如果觸發器已經存在,則報錯.
            scheduler.scheduleJob(trigger);
        }
    }
 /**
     * 內部方法:處理 Trigger
     * @param schedulerEntity
     * @return
     */
    private Trigger getTrigger(SchedulerEntity schedulerEntity, JobKey jobKey) {
        //觸發器參數
        //schedulerEntity 中 job_data 屬性值必須設置爲 json 字符串格式,所以這裏轉爲 JobDataMap 對象.
        JobDataMap triggerDataMap = new JobDataMap();
        Map<String, Object> triggerData = schedulerEntity.getTrigger_data();
        if (triggerData != null && triggerData.size() > 0) {
            triggerDataMap.putAll(triggerData);
        }
        //如果觸發器名稱爲空,則使用 UUID 隨機生成. group 爲null時,會默認爲 default.
        if (StringUtils.isBlank(schedulerEntity.getTrigger_name())) {
            schedulerEntity.setTrigger_name(UUID.randomUUID().toString());
        }
        //過期執行策略採用:MISFIRE_INSTRUCTION_DO_NOTHING
        //forJob:爲觸發器關聯作業. 一個觸發器只能關聯一個作業.
        TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
        triggerBuilder.withIdentity(schedulerEntity.getTrigger_name(), schedulerEntity.getTrigger_group());
        triggerBuilder.withDescription(schedulerEntity.getTrigger_desc());
        triggerBuilder.usingJobData(triggerDataMap);
        if (jobKey != null && jobKey.getName() != null) {
            triggerBuilder.forJob(jobKey);
        }
        triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(schedulerEntity.getCron_expression())
                .withMisfireHandlingInstructionDoNothing());
        return triggerBuilder.build();
    }
    /**
     * 內部方法:處理 JobDetail
     * storeDurably(boolean jobDurability):指示 job 是否是持久性的。如果 job 是非持久的,當沒有活躍的 trigger 與之關聯時,就會被自動地從 scheduler 中刪除。即非持久的 job 的生命期是由 trigger 的存在與否決定的.
     * requestRecovery(boolean jobShouldRecover) :指示  job 遇到故障重啓後,是否是可恢復的。如果 job 是可恢復的,在其執行的時候,如果 scheduler 發生硬關閉(hard shutdown)(比如運行的進程崩潰了,或者關機了),則當 scheduler 重啓時,該 job 會被重新執行。
     *
     * @param schedulerEntity
     * @return
     * @throws ClassNotFoundException
     */
    private JobDetail getJobDetail(SchedulerEntity schedulerEntity) throws ClassNotFoundException {
        //如果任務名稱爲空,則使用 UUID 隨機生成.
        if (StringUtils.isBlank(schedulerEntity.getJob_name())) {
            schedulerEntity.setJob_name(UUID.randomUUID().toString());
        }
        Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(schedulerEntity.getJob_class_name());
        //作業參數
        JobDataMap jobDataMap = new JobDataMap();
        Map<String, Object> jobData = schedulerEntity.getJob_data();
        if (jobData != null && jobData.size() > 0) {
            jobDataMap.putAll(jobData);
        }
        //設置任務詳情.
        return JobBuilder.newJob(jobClass)
                .withIdentity(schedulerEntity.getJob_name(), schedulerEntity.getJob_group())
                .withDescription(schedulerEntity.getJob_desc())
                .usingJobData(jobDataMap)
                .storeDurably(true)
                .requestRecovery(true)
                .build();
    }

src/main/java/com/wmx/quartzjdbc/controller/SchedulerController.java

src/main/java/com/wmx/quartzjdbc/service/SchedulerService.java

Quartz Scheduler 配置集羣

1、現在的應用通常都採用多實例部署,使用集羣的方式減輕服務器壓力,防止單臺服務器宕機而導致服務不可用。Quartz 的集羣功能通過故障轉移和負載均衡功能爲調度程序帶來高可用性和可擴展性。

2、quartz 集羣適用JDBC 持久化(JobStoreTX 或 JobStoreCMT),並且基本上都是通過集羣的每個節點共享相同的數據庫來工作,即大家訪問同一個數據。

3、quartz 集羣有自己的負載均衡策略,每個節點都儘可能快地觸發作業,當 Triggers 的觸發時間發生時,獲取到它的第一個節點(通過在其上放置一鎖定)是將觸發它的節點。

4、當某個節點出現故障時,其他節點會檢測到該狀況並識別數據庫中在故障節點內正在進行的作業,任何標記爲恢復的作業(requestRecovery=true)將被剩餘的節點重新執行。沒有標記爲恢復的作業將在下一次相關的 Triggers 觸發時執行。

5、集羣功能最適合擴展長時間運行、或 cpu 密集型作業,通過多個節點分配工作負載,減輕服務器壓力。

6、配置集羣只需在非集羣的基礎上加上如下兩項配置即可:

org.quartz.jobStore.isClustered=true #爲 true 表示開啓集羣功能,默認爲 false
org.quartz.scheduler.instanceId=auto #集羣的每個節點名稱必須唯一,可以手動指定,也可設置爲 "AUTO" 此時自動命名.

7、官方建議集羣下的各個實例應該使用相同的 quartz.properties 文件,也就是使用相同的 quartz 配置,instanceId 配置項除外。quartz 官網集羣配置

8、下面測試 quartz 集羣,使用 IDEA 將同一個應用並行啓動,即啓動一次後,修改服務器端口,然後再繼續啓動:

配置文件源碼:https://github.com/wangmaoxiong/quartzjdbc/tree/master/src/main/resources
application.yml 非集羣配置
application-cluster.yml 集羣配置,使用時改下文件名稱。

測試結果顯示:2個 quartz 實例同時啓動,當觸發任務時,只會有一個實例執行作業,其它實例不會執行;當其中一個宕機時,另一個會繼續執行調度。

十:後記:

1、至此文章結束,github 源碼中都有詳細註釋。作爲後臺開發,專注於提供規範的接口即可,所以示例中並未提供操作頁面。

2、通過封裝調度控制層與業務層之後,後續新增任務執行邏輯時,則只需要添加 Job 實現類即可,然後通過接口註冊啓動。

3、因爲 qurtz 自身已經提供了 11 張表,所以本文直接提取了表中的字段封裝爲實體對象,沒有再新建自己的表,生產中根據實際情況進行設計。

 

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