Quartz 使用教程

小二十七 發表於 2023-01-22

首先說說,為什麼要寫這篇文章:

  • 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 的工作流程也很簡單,大致如下:

  1. 首頁基於 Job 介面定義你的作業 JobDetail 例項和觸發器 Trigger 例項物件
  2. 將定義的作業和觸發器例項物件透過排程器 scheduleJob,開始排程執行
  3. 排程器啟動工作執行緒開始執行 JobDetail 例項的 execute 方法內容
  4. 任務執行時所需資訊透過,JobExecutionContext 物件傳遞到工作執行緒,也可以在多個工作執行緒中跨執行緒傳遞

示意圖:

Quartz Workflow

唯一標識

關於建立 JobDetail 作業例項和 Trigger 觸發器的幾個注意事項:

  1. 建立作業和觸發器都需要透過(JobKey 和 TriggerKey + Group)組合建立唯一標識
  2. 你可以透過唯一標識在 Schduler 中獲取作業物件,並且管理和維護他們
  3. 引入 Group 標識的目的也是了更好的讓你管理作業環境:例如:透過不同的 Group 來區分:【測試作業,生產作業】等

JobDetail 的更多細節

透過示例可以看到,定義和使用 Job 都非常簡單,但是如果要深入使用,你可能需要了解關於 Job 的更多細節

先看看 Quartz 對於 JobDetail 的處理策略:

  1. 每次執行任務都會建立一個新的 JobDetail 例項物件,意味每次執行的 JobDetail 都是新物件,JobDetail 物件也是無狀態的
  2. JobDetail 例項物件任務完成後 (execute 方法),排程器 Schduler 會將作業例項物件刪除,然後進行垃圾回收
  3. 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 的使用,需要關注以下的注意事項:

  1. 雖然 JobDataMap 可以傳遞任意型別的資料,物件的反序列化在版本迭代中容易遇到類版本控制的問題
  2. 如果從長遠的安全性考慮,儘可能的將 jobDataMap 設定為只允許存放基本型別和字串(透過 jobStore.useProperties 設定)
  3. 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 是適用於大多數場景的觸發器,它可以指定特定時間,重複間隔,重複次數等簡單場景,它主要設定引數如下:

  1. 開始時間
  2. 結束時間
  3. 重複次數
  4. 間隔時間

具體的 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);

上述程式碼完成以下幾件事情:

  1. 建立 Cron 表示式:每天上午 8 點到下午 5 點之間每隔一分鐘觸發一次
  2. 指定 MISFIRE_INSTRUCTION_FIRE_NOW 為 CronTrigger 的處理策略
  3. 透過 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);
}

想要實現監聽,需要以下幾步:

  1. 自定義監聽類,實現 *Listener 監聽介面
  2. 在你感興趣的事件,加入你的邏輯程式碼
  3. 將自定義監聽類,在任務排程前,註冊到 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 表已經新增資料,如下:

image-20230121111017024

注意事項

在使用 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 如下:

starter-quartz

啟動 Springboot 會發現,無需任何配置就已經整合 Quartz 模組了:

image-20230121215546009

使用示例

現在基於整合模式實現剛才的 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 中選項:

  1. 在控制層 Controller 提供介面,手動接收任務指定
  2. 監聽 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 天生支援基於資料庫的分散式:

JDBC-JobStore配置群集

要啟用叢集模式,需要注意以下事項:

  1. 需要啟用 JDBCStore 或者 TerracottaJobStore 執行模式
  2. 需要將 jobStore.isClustered 屬性設定為 True
  3. 每個單獨例項需要設定唯一的 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 啟動叢集模式執行:

quartz clustered

注意事項

使用叢集模式,需要注意以下事項:

  • 不要在單機模式下使用叢集模式,不然會出現時鐘同步問題,具體參考 NIST Internet Time Service (ITS) | NIST
  • 不要在叢集示例中,執行單機示例,不然會出現資料混亂和不穩定的情況
  • 關於任務的執行節點是隨機的(哪個節點搶到鎖就可以執行),尤其對大量情人的情況
  • 如果不想依賴 JDBC 資料庫實現叢集,可以看看 TerracottaJobStore 模式

以上對於 Quartz 的總結就到這裡了,有什麼不當之處,歡迎交流指正。