首先說說,爲什麼要寫這篇文章:
- Quartz 的
v2.3.2
版本改動比較大,目前網上的資料都是舊版本,很缺乏相關資料 - 很多資料講解非常不全面,例如 Quartz Listener 的介紹和使用基本缺失
- Springboot 整合 Quartz 是目前普遍的使用場景,但是 Quartz 官方沒有相關資料
- 網上很少關於基於 Springboot 整合的 Quartz 搭建集羣環境,而且大多無法運行
爲了避免讓大家重複踩坑,綜上所述就是我要寫本篇文章的目的了
本文檔編寫時間 23.1.17,基於目前最新的穩定版 quartz 2.3.2 實現
概述
簡單介紹:
Quartz 是目前 Java 領域應用最爲廣泛的任務調度框架之一,目前很多流行的分佈式調度框架,例如 xxl-job 都是基於它衍生出來的,所以瞭解和掌握 Quartz 的使用,對於平時完成一些需要定時調度的工作會更有幫助
快速開始
我們通過一個最簡單的示例,先快速上手 Quartz 最基本的用法,然後再逐步講解 Quartz 每個模塊的功能點
第一步:添加依賴
在 pom.xml
文件添加 Quart 依賴:
<!-- 引入 quartz 基礎依賴:可取當前最新版本 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<!-- 引入 quartz 所需的日誌依賴:可取當前最新版本 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.26</version>
<scope>compile</scope>
</dependency>
第二步:配置文件
在項目的 classpath
路徑下創建 Quartz 默認的 quartz.properties
配置文件,它看起來像這樣:
# 調度程序的名稱
org.quartz.scheduler.instanceName = MyScheduler
# 線程數量
org.quartz.threadPool.threadCount = 3
# 內存數據庫(推薦剛上手時使用)
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
第三步:定義任務類
實現 Job
接口,然後在覆蓋的 execute
函數內定義任務邏輯,如下:
package org.example.quartz.tutorial;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println("hello quartz!");
}
}
第四步:任務調度
我們簡單的使用 main()
方法即可運行 Quartz 任務調度示例:
package org.example.quartz.tutorial;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
public class QuartzTest {
public static void main(String[] args) {
try {
// 獲取默認的調度器實例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 打開調度器
scheduler.start();
// 定義一個簡單的任務
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job11", "group1")
.build();
// 定義一個簡單的觸發器: 每隔 1 秒執行 1 次,任務永不停止
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever()
).build();
// 開始調度任務
scheduler.scheduleJob(job, trigger);
// 等待任務執行一些時間
Thread.sleep(3000);
// 關閉調度器
scheduler.shutdown();
} catch (Exception se) {
se.printStackTrace();
}
}
}
最後控制檯會輸出任務運行的全過程,然後關閉進程,如下:
[main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
[main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
[main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
[main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
[main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'MyScheduler' with instanceId 'NON_CLUSTERED'
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads.
Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.
[main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'MyScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
[main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
[main] INFO org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED started.
hello quartz!
hello quartz!
hello quartz!
hello quartz!
[main] INFO org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED shutting down.
[main] INFO org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED paused.
[main] INFO org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED shutdown complete.
Process finished with exit code 0
可以看到,到這裏一個最基本簡單的 Quartz 使用示例基本就 OK 了,接下來介紹更多核心概念和深入使用的場景
核心概念
觸發器和作業
基本概念
掌握 Quartz 之前,先來了解它框架的 3 個核心組件和概念,如下:
- Schduler:調度器,主要用於管理作業(JobDetail),觸發器(Trigger)
- JobDetail:作業實例,內部包含作業運行的具體邏輯
- Trigger:觸發器實例,內部包含作業執行的實踐計劃
工作流程
如上所示,使用 Quartz 的工作流程也很簡單,大致如下:
- 首頁基於
Job
接口定義你的作業JobDetail
實例和觸發器Trigger
實例對象 - 將定義的作業和觸發器實例對象通過調度器
scheduleJob
,開始調度執行 - 調度器啓動工作線程開始執行
JobDetail
實例的execute
方法內容 - 任務運行時所需信息通過,
JobExecutionContext
對象傳遞到工作線程,也可以在多個工作線程中跨線程傳遞
示意圖:
唯一標識
關於創建 JobDetail 作業實例和 Trigger 觸發器的幾個注意事項:
- 創建作業和觸發器都需要通過(JobKey 和 TriggerKey + Group)組合創建唯一標識
- 你可以通過唯一標識在 Schduler 中獲取作業對象,並且管理和維護他們
- 引入 Group 標識的目的也是了更好的讓你管理作業環境:例如:通過不同的 Group 來區分:【測試作業,生產作業】等
JobDetail 的更多細節
通過示例可以看到,定義和使用 Job 都非常簡單,但是如果要深入使用,你可能需要了解關於 Job 的更多細節
先看看 Quartz 對於 JobDetail 的處理策略:
- 每次執行任務都會創建一個新的 JobDetail 實例對象,意味每次執行的 JobDetail 都是新對象,JobDetail 對象也是無狀態的
- JobDetail 實例對象任務完成後 (execute 方法),調度器 Schduler 會將作業實例對象刪除,然後進行垃圾回收
- JobDetail 實例之間的狀態數據,只能通過
JobExecutionContext
(實際上是 JobDataMap) 進行跨作業傳遞
JobDataMap
jobDataMap 的使用主要分 2 步:
1:在 execute()
函數內,使用 jobDataMap 獲取數據
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) {
// 通過 JobDataMap 對象,可以在作業的執行邏輯中,獲取參數
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
String name = jobDataMap.getString("name");
System.out.println("hello " + name);
}
}
2:jobDataMao 添加參數
// 定義作業時,通過 usingJobData 將參數放入 JobDataMap
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job11", "group1")
.usingJobData("name", "phoenix")
.build();
最後運行效果如下:
[main] INFO org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED started.
hello phoenix
[main] INFO org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED shutting down.
# ....
關於 JobDataMap 的使用,需要關注以下的注意事項:
- 雖然 JobDataMap 可以傳遞任意類型的數據,對象的反序列化在版本迭代中容易遇到類版本控制的問題
- 如果從長遠的安全性考慮,儘可能的將 jobDataMap 設置爲只允許存放基本類型和字符串(通過
jobStore.useProperties
設置) - Quartz 會自動通過 Job 類的 setter 方法和 JobDataMap 主鍵匹配,幫助你自動注入屬性到 Job 類中
併發性和持久化
Quartz 對於 Job 提供幾個註釋,合理的使用可以更好的控制 Quartz 的調度行爲,具體如下:
-
@DisallowConcurrentExecution:添加到 Job 類中,告訴 Job 防止相同定義的任務併發執行,例如:任務 A 實例未完成任務,則任務 B 實例不會開始執行(Quartz 默認策略是不會等待,啓用新線程併發調度)
-
@PersistJobDataAfterExecution:添加到 Job 類中,默認情況下 Job 作業運行邏輯不會影響到 JobDataMap (既每個 JobDetail 拿到的都是初始化的 JobDataMap 內容),開啓該註解後,Job 的
execute()
方法完成後,對於 JobDataMap 的更新,將會被持久化到 JobDataMap 中,從而供其他的 JobDetail 使用,這對於任務 B 依賴任務 A 的運行結果的場景下,非常有用,所以強烈建議和@DisallowConcurrentExecution
註解一起使用,會讓任務運行結果更加符合預期
使用示例如下:
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class QuartzTest {
public static void main(String[] args) {
//.....
}
}
Trigger 的更多細節
和 Job 類似,觸發器的定義和使用也非常簡單,但是如果想充分的利用它來工作,你還需要了解關於觸發器的更多細節
在 Quartz 中 Trigger 觸發器有很多種類型,但是他們都有幾個共同的屬性,如下:
- startTime:觸發器首次生效的時間
- endTime:觸發器失效時間
以上共同屬性的值都是 java.util.Date
對象
關於 Trigger 的其他幾個概念:
- Priority 優先權:當調度器遇到多個同時執行的 Trigger 時候,會根據優先權大小排序,然後先後調度
- Misfire 錯過觸發:Trigger 達到觸發時間,但因爲外部原因無法執行,Trigger 開始計算 Misfire 時間
- 常見的外部原因有哪些?例如:調度程序被關閉,線程池無可用工作線程等
- Calendar 日曆(不是 java.util.calendar 對象):用於排除執行日期非常有用
- 例如:定義一個每天 9 點執行 Trigger ,但是排除所有法定節假日
SimpleTrigger
SimpleTrigger 是適用於大多數場景的觸發器,它可以指定特定時間,重複間隔,重複次數等簡單場景,它主要設定參數如下:
- 開始時間
- 結束時間
- 重複次數
- 間隔時間
具體的 API 可以參考 Quartz 的 Java Doc 文檔,這裏就不贅述了
misfire 處理策略:
我們上面說過 Quartz Misfire 的概念,從源碼 SimpleScheduleBuilder
類中可以看到 MISFIRE_INSTRUCTION_SMART_POLICY
是默認的觸發策略,但是也我們也可以在創建 Trigger 時候設置我們期望的錯過觸發策略,如下:
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever()
// misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW;
.withMisfireHandlingInstructionFireNow()
).build();
在 SimpleTrigger
類中的常量可以看到所有錯過觸發(misfire)處理邏輯:
MISFIRE_INSTRUCTION_FIRE_NOW
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
關於 misfire 的具體的行爲,可以查閱 Quartz 的 Java Doc 文檔
CronTrigger
相比 SimpleTrigger 可以指定更爲複雜的執行計劃,CRON 是來自 UNIX
基於時間的任務管理系統,相關內容就不再展開,可以參閱 Cron - (wikipedia.org) 文檔進一步瞭解,
Cron 也有類似 SimpleTrigger 的相同屬性,設置效果如下:
- startTime:觸發器首次生效的時間
- endTime:觸發器失效時間
看看 CronTrigger 的使用示例:
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(CronScheduleBuilder
.cronSchedule("0 0/2 8-17 * * ?")
.withMisfireHandlingInstructionFireAndProceed()
)
.build();
scheduler.scheduleJob(job, cronTrigger);
上述代碼完成以下幾件事情:
- 創建 Cron 表達式:每天上午 8 點到下午 5 點之間每隔一分鐘觸發一次
- 指定 MISFIRE_INSTRUCTION_FIRE_NOW 爲 CronTrigger 的處理策略
- 通過 Schduler 對任務開始進行調度
CronTrigger Misfire 策略定義在 CronTrigger
常量中,可以在 Java Doc 文檔中查看其具體的行爲
Linstener 監聽器
監聽器用於監聽 Quartz 任務事件執行對應的操作,大致分類如下:
- JobListener:用於監聽 JobDetail 相關事件
- TriggerListener:用於監聽 Trigger 相關事件
- SchdulerListener:用於監聽 Schduler 相關事件
在常見的 JobListener 接口中,提供以下事件監聽:
public interface JobListener {
public String getName();
// 作業即將開始執行時觸發
public void jobToBeExecuted(JobExecutionContext context);
// 作業即將取消時通知
public void jobExecutionVetoed(JobExecutionContext context);
// 作業執行完成後通知
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException);
}
想要實現監聽,需要以下幾步:
- 自定義監聽類,實現 *Listener 監聽接口
- 在你感興趣的事件,加入你的邏輯代碼
- 將自定義監聽類,在任務調度前,註冊到 Schduler 中即可
在 Schduler 中註冊一個對所有任務生效的 Listener 的示例:
scheduler.getListenerManager().addJobListener(myJobListener, allJobs());
關於使用 Listener 的建議:
- 在最新的
2.3.2
Listener 不會存儲在 JobStore 中,所以在持久化模式下,每次啓動都需要重新註冊監聽 - 大多數場景下 Quartz 用戶不會使用 Listener,除非非常必要的情況才使用
JobStore 作業存儲
JobStore 屬性在 Quartz 配置文件中聲明,用於定義 Quartz 所有運行時任務的存儲方式,目前主要有兩種方式
RAMJobStore
RAMJobStore 是基於內存的存儲模式,其特點如下:
- 優點:使用,配置簡單,性能最高
- 缺點:程序關閉後,任務信息會丟失
配置方式如下:
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
JDBCJobStore
JDBCJobStore 是基於數據的存儲模式,其特點如下:
- 優點:支持常見的數據庫,可以持久化保存任務信息
- 缺點:配置繁瑣,性能不高(取決於數據庫)
使用示例
使用 JDBCJobStore 需要以下 3 步完成:
第一步:在項目中添加相關數據庫依賴:
<!-- 添加數據庫依賴 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
第二步:在數據庫執行 [Quartz 官方的 SQL DDL 腳本](quartz/tables_mysql_innodb.sql at master · quartz-scheduler/quartz (github.com)),創建數據庫表結構,Quartz 核心的表結構如下:
Table Name | Description |
---|---|
QRTZ_CALENDARS | 存儲Quartz的Calendar信息 |
QRTZ_CRON_TRIGGERS | 存儲CronTrigger,包括Cron表達式和時區信息 |
QRTZ_FIRED_TRIGGERS | 存儲與已觸發的Trigger相關的狀態信息,以及相聯Job的執行信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存儲已暫停的Trigger組的信息 |
QRTZ_SCHEDULER_STATE | 存儲少量的有關Scheduler的狀態信息,和別的Scheduler實例 |
QRTZ_LOCKS | 存儲程序的悲觀鎖的信息 |
QRTZ_JOB_DETAILS | 存儲每一個已配置的Job的詳細信息 |
QRTZ_JOB_LISTENERS | 存儲有關已配置的JobListener的信息 |
QRTZ_SIMPLE_TRIGGERS | 存儲簡單的Trigger,包括重複次數、間隔、以及已觸的次數 |
QRTZ_BLOG_TRIGGERS | Trigger作爲Blob類型存儲 |
QRTZ_TRIGGERS | 存儲已配置的Trigger的信息 |
第三步:配置文件修改爲 JDBCJobStore 模式,配置數據源,並且將 jobStore
指定爲該數據源,如下
# quartz scheduler config
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 3
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = myDS
# dataSource
org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://127.0.0.1:3306/quartz_demo
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = test123456
org.quartz.dataSource.myDS.maxConnections = 30
最後運行 QuartzTest 後可以看到數據庫 QRTZ_JOB_DETAILS 表已經添加數據,如下:
注意事項
在使用 JDBCJobStore 時,需要注意以下事項:
- Quartz 的 JobStoreTX 默認是獨立示例,如果需要和其他事務一起工作(例如 J2EE 服務器),可以選擇
JobStoreCMT
- 默認表前綴是
QRTZ_
,可進行配置,使用多個不同的前綴有助於實現同一數據庫的任務調度多組表結構 - JDBC 委託驅動
StdJDBCDelegate
適用於大多數數據庫,目前只針對測試StdJDBCDelegate
時出現問題的類型進行特定的委託- DB2v6Delegate:適用於 DB2 版本 6 及更早版本
- HSQLDBDelegate:適用於 HSQLDB 數據庫
- MSSQLDelegate:適用於 Microsoft SQLServer 數據庫
- PostgreSQLDelegate:適用於 PostgreSQL 數據庫
- WeblogicDelegate:由 Weblogic 製作的驅動程序
- OracleDelegate:適用於 Oracle 數據庫
- …………
- 將
org.quartz.jobStore.useProperties
設置爲 True,避免將非基礎類型數據存儲到數據庫的 BLOB 字段
springboot 集成
Quartz 整合 Springboot 非常普遍的場景,整合 Spring 可以帶來好處:
- 更加簡潔的配置,開箱即用
- 和 Spring 的 IOC 容器融合,使用更便捷
添加依賴
可以在現有項目上添加 springboot 官方提供的 starter-quartz 依賴,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
如果是新項目,可以直接在 Spring Initializr 添加 Quartz Schduler 如下:
啓動 Springboot 會發現,無需任何配置就已經整合 Quartz 模塊了:
使用示例
現在基於整合模式實現剛纔的 Demo 示例,首先定義任務,這裏不再是實現 Job 類:
public class HelloJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
String name = jobDataMap.getString("name");
System.out.println("Hello :" + name);
}
}
這裏實現由 Springboot 提供的 QuartzJobBean,實現 executerInternal()
方法,這是一個經過 Spring 容器包裝後的任務類,可以在任務類使用 Spring 容器的實例
在 Demo 示例裏面,我們調度啓動都是在 Main 方法啓動,在本地測試沒有問題,但在生產環境就不建議了,和 springboot 整合後關於任務執行,現在可以有 2 中選項:
- 在控制層 Controller 提供接口,手動接收任務指定
- 監聽 Spring 容器,在容器啓動後,自動加載任務,並且註冊爲 Bean
手動執行
我們先看看第一種實現方式,我們創建控制器,然後接收參數,創建任務,如下:
@RestController
public class HelloController {
@Autowired
private Scheduler scheduler;
@GetMapping("/hello")
public void helloJob(String name) throws SchedulerException {
// 定義一個的任務
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job11", "group1")
.usingJobData("name", name)
.build();
// 定義一個簡單的觸發器: 每隔 1 秒執行 1 次,任務永不停止
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever()
).build();
// 開始調度
scheduler.scheduleJob(job, trigger);
}
}
然後啓動服務器,訪問接口傳入參數:
$curl --location --request GET 'http://localhost:8080/hello?name=phoenix'
然後控制檯會輸出:
2023-01-21 22:03:03.213 INFO 23832 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-01-21 22:03:03.213 INFO 23832 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-01-21 22:03:03.214 INFO 23832 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
Hello :phoenix
Hello :phoenix
#....
自動執行
將 JobDetail 註冊 Bean,任務就會隨 Spring 啓動自動觸發執行,這對於需要隨程序啓動執行的作業非常有效,配置如下:
先創建一個配置類:
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail() {
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job11", "group1")
.usingJobData("name", "springboot")
.storeDurably()
.build();
return job;
}
@Bean
public Trigger trigger() {
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail())
.withIdentity("trigger1", "group1")
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever()
).build();
return trigger;
}
}
然後在 springboot 啓動後,任務就自動執行:
2023-01-21 22:29:51.962 INFO 46376 --- [ main] org.quartz.core.QuartzScheduler : Scheduler quartzScheduler_$_NON_CLUSTERED started.
Hello :springboot
Hello :springboot
Hello :springboot
# ....
集羣模式
對於生產環境來說,高可用,負載均衡,故障恢復,這些分佈式的能力是必不可少的,Quartz 天生支持基於數據庫的分佈式:
要啓用集羣模式,需要注意以下事項:
- 需要啓用 JDBCStore 或者 TerracottaJobStore 運行模式
- 需要將
jobStore.isClustered
屬性設置爲 True - 每個單獨實例需要設置唯一的
instanceId
(Quartz 提供參數讓這點很容易實現)
配置集羣
下面看看 springboot 集成的模式下如何配置 quartz 集羣模式:
在 application.yml
添加 quartz 集羣配置信息:
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
password: 123456
url: jdbc:mysql://127.0.0.1:3306/quartz_demo
username: root
quartz:
job-store-type: jdbc
properties:
org:
quartz:
scheduler:
instanceName: ClusteredScheduler # 集羣名,若使用集羣功能,則每一個實例都要使用相同的名字
instanceId: AUTO # 若是集羣下,每個 instanceId 必須唯一,設置 AUTO 自動生成唯一 Id
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 25
threadPriority: 5
jobStore:
class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
useProperties: true # 使用字符串參數,避免了將非 String 類序列化爲 BLOB 的類版本問題
isClustered: true # 打開集羣模式
clusterCheckinInterval: 5000 # 集羣存活檢測間隔
misfireThreshold: 60000 # 最大錯過觸發事件時間
使用集羣模式需要添加數據庫依賴,如下:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
然後創建 SchedulerConfig
配置類,將相關的配置信息加載到 SchedulerFactoryBean
中才能生效:
@Configuration
public class SchedulerConfig {
@Autowired
private DataSource dataSource;
@Autowired
private QuartzProperties quartzProperties;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
Properties properties = new Properties();
properties.putAll(quartzProperties.getProperties());
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setOverwriteExistingJobs(true);
factory.setDataSource(dataSource);
factory.setQuartzProperties(properties);
return factory;
}
}
最後在啓動日誌內,可以看到 Quartz 啓動集羣模式運行:
注意事項
使用集羣模式,需要注意以下事項:
- 不要在單機模式下使用集羣模式,不然會出現時鐘同步問題,具體參考 NIST Internet Time Service (ITS) | NIST
- 不要在集羣示例中,運行單機示例,不然會出現數據混亂和不穩定的情況
- 關於任務的運行節點是隨機的(哪個節點搶到鎖就可以執行),尤其對大量情人的情況
- 如果不想依賴 JDBC 數據庫實現集羣,可以看看 TerracottaJobStore 模式
以上對於 Quartz 的總結就到這裏了,有什麼不當之處,歡迎交流指正。