在資料庫中持久化 Quartz 調度器
1. 簡介
在建立 Spring Web 應用程式時,我們經常需要安排重複執行的任務或作業,例如發送電子郵件、產生報告或按特定間隔處理資料。 Quartz Scheduler 因其強大而靈活的調度功能而成為處理此類任務的熱門選擇。
Spring Web 應用程式的一個關鍵挑戰是確保已調度的 Quartz 作業在應用程式重新啟動後仍然持久化,並無縫維護其狀態和調度。通常有兩種方法可以實現這一點:
- 讓 Quartz 本身使用其 JDBC JobStore 處理持久性。
- 在自訂業務表中維護作業定義,並在啟動時將其載入到 Quartz 中。
在本教程中,我們將探討這兩種方法。
2. 問題
在 Spring Web 應用程式中,開發人員可以整合 Quartz 來管理排程作業。一個關鍵要求是將作業和觸發器的詳細資訊儲存在資料庫中,以便在應用程式關閉或重新啟動時不會遺失。
在生產環境中,應用程式經常會因為維護、更新或意外崩潰而重新啟動。如果 Quartz 作業僅儲存在記憶體中(預設行為),則它們會在重新啟動期間遺失,導致執行失敗或需要手動重新調度。
將作業持久化到資料庫中可以確保作業的連續性,使調度程序能夠從中斷的地方繼續執行。這對於必須按照精確時間表運行的任務(例如每日報告或時效性通知)尤其重要。
3. Maven依賴項
讓我們先將spring-boot-starter-quartz
、spring-boot-starter-data-jdbc
、 spring-boot-starter-data-jpa
和h2
依賴項匯入到我們的pom.xml
中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
4.使用Quartz的JDBC JobStore
Quartz 透過自己的模式( QRTZ_*
表)提供內建持久性。當使用 JDBC 配置時,Quartz 會自動將所有作業、觸發器和排程元資料儲存在資料庫中:
spring.quartz.job-store-type=jdbc
在開發過程中,我們可以使用基於 H2 檔案的資料庫來模擬重啟後的持久化。建議的方法是讓 Spring Boot 建立一次模式,然後停用模式初始化,這樣我們的作業資料就可以保留下來:
spring.datasource.url=jdbc:h2:file:~/quartz-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# First run: let Spring create the Quartz schema
spring.quartz.jdbc.initialize-schema=always
# Restart runs: disable schema initialization to preserve existing data
# spring.quartz.jdbc.initialize-schema=never
使用此設置,Quartz 在首次運行時將在~/quartz-db.mv.db
中建立所需的QRTZ_*
表。在後續重新啟動時,我們將設定切換為“ never
”,這樣現有的表格和作業資料就不會被刪除或重新初始化,從而允許 Quartz 自動重新載入已排程的作業。這樣,作業在應用程式重新啟動後仍能繼續運行,我們可以使用一個簡單的基於 H2 檔案的資料庫安全地測試復原行為。
4.1. 定義 Quartz 作業
讓我們透過實作Job
介面來建立一個作業:
public class SampleJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println("Executing SampleJob at " + System.currentTimeMillis());
}
}
然後,我們使用JobDetail
類別定義SampleJob
的實例:
@Bean
public JobDetail sampleJobDetail() {
return JobBuilder.newJob(SampleJob.class)
.withIdentity("sampleJob", "group1")
.storeDurably()
.requestRecovery(true)
.build();
}
我們使用storeDurably()
將作業持久化到 Quartz 資料庫中。此外,我們設定了requestRecovery(true)
以確保在應用程式執行過程中崩潰時能夠重試。
現在,我們需要定義一個觸發器:
@Bean
public Trigger sampleTrigger(JobDetail sampleJobDetail) {
return TriggerBuilder.newTrigger()
.forJob(sampleJobDetail)
.withIdentity("sampleTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/30 * * * * ?")) // every 30s
.build();
}
我們用 cron 表達式( 0/30 * * * * ?
)定義觸發器,安排作業每30
秒執行一次。
4.2. 重啟時重新初始化作業
當 Quartz 使用 JDBC 作業儲存時, JobDetail
及其關聯的Trigger
都會持久保存在 Quartz 的 schema 中。應用程式重新啟動時,Quartz 會自動從資料庫重新載入它們,因此無需自訂初始化邏輯。
讓我們建立一個單元測試:
@Test
void givenSampleJob_whenSchedulerRestart_thenSampleJobIsReloaded() throws Exception {
// Given
JobKey jobKey = new JobKey("sampleJob", "group1");
TriggerKey triggerKey = new TriggerKey("sampleTrigger", "group1");
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
assertNotNull(jobDetail, "SampleJob exists in running scheduler");
Trigger trigger = scheduler.getTrigger(triggerKey);
assertNotNull(trigger, "SampleTrigger exists in running scheduler");
// When
scheduler.standby();
Scheduler restartedScheduler = applicationContext.getBean(Scheduler.class);
restartedScheduler.start();
// Then
assertTrue(restartedScheduler.isStarted(), "Scheduler should be running after restart");
JobDetail reloadedJob = restartedScheduler.getJobDetail(jobKey);
assertNotNull(reloadedJob, "SampleJob should be reloaded from DB after restart");
Trigger reloadedTrigger = restartedScheduler.getTrigger(triggerKey);
assertNotNull(reloadedTrigger, "SampleTrigger should be reloaded from DB after restart");
}
此測試確保 Quartz 在模擬重新啟動後能夠正確地從其持久性儲存中重新載入作業和觸發器。首先,我們驗證正在執行的調度程式中是否存在sampleJob
及其關聯的sampleTrigger
。
接下來,我們將調度器置於待機模式,並從 Spring 上下文中取得一個新的Scheduler
實例,模擬應用程式重新啟動。啟動新的調度器後,我們斷言作業和觸發器都再次可用。這證實了 Quartz 會自動從資料庫復原排程任務,無需額外的初始化邏輯。
5. 使用自訂業務作業儲存庫
雖然 Quartz 提供了內建的持久化功能,但有時這還不夠。在許多應用程式中,我們需要對作業的業務生命週期進行更多控制。例如,將它們標記為已啟用、已停用或已完成。在這些情況下,Quartz 的作業儲存無法擷取足夠的業務上下文,因此我們引入了自己的表格來明確管理作業。
5.1. 定義自訂作業表
我們可以先建立一個 JPA 實體來表示我們業務領域的工作:
@Entity
public class ApplicationJob {
@Id
private Long id;
private String name;
private boolean enabled;
private Boolean completed;
}
表格獨立於 Quartz 的QRTZ_*
模式,完全由我們控制。它允許我們追蹤 Quartz 無法處理的作業元數據,例如作業是否已明確標記為已完成。
5.2. 播種商業工作
首次啟動時,我們可以預先在表中填入作業定義。例如,一個簡單的播種機可能會在表為空時插入一筆記錄:
@Component
public class DataSeeder implements CommandLineRunner {
private final ApplicationJobRepository repository;
public DataSeeder(ApplicationJobRepository repository) {
this.repository = repository;
}
@Override
public void run(String... args) {
if (repository.count() == 0) {
ApplicationJob job = new ApplicationJob();
job.setName("simpleJob");
job.setEnabled(true);
job.setCompleted(false);
repository.save(job);
}
}
}
此步驟模擬一個真實的系統,其中可以透過管理 UI 或業務工作流程定義或管理作業。
5.3. 啟動時重新初始化作業
接下來,我們需要將業務儲存庫與 Quartz 連接起來。在應用程式啟動時,監聽器可以查詢ApplicationJob
表並動態排程作業:
@Component
public class JobInitializer implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private ApplicationJobRepository jobRepository;
@Autowired
private Scheduler scheduler;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
for (ApplicationJob job : jobRepository.findAll()) {
if (job.isEnabled() && (job.getCompleted() == null || !job.getCompleted())) {
JobDetail detail = JobBuilder.newJob(SampleJob.class)
.withIdentity(job.getName(), "appJobs")
.storeDurably()
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(detail)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(30)
.repeatForever())
.build();
try {
scheduler.scheduleJob(detail, trigger);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
}
}
}
重新啟動後,Quartz 不會自動重新載入這些作業,因為它們沒有儲存在 Quartz 自己的 schema 中。相反, JobInitializer
會運行,查詢業務表,並確保只有標記為已啟用但尚未完成的作業才會在 Quartz 中被調度。
JobInitializer
使用scheduler.scheduleJob()
方法重新排程作業。
與 Quartz 內建的持久化機制相比,這種方式雖然重複了調度邏輯,但卻提供了更細緻的控制。當作業定義屬於業務模型的一部分,並且需要與其他領域資料一起管理時,這種方式尤其有用。
6. 結論
在本文中,我們探討了兩種持久化和恢復 Quartz 作業的方法。 Quartz 內建的 JDBC 持久化功能提供了一個交鑰匙解決方案,它會自動將作業和觸發器儲存在自己的模式中,並在應用程式重新啟動後無縫地重新載入它們。
另一方面,自訂業務作業儲存庫使我們能夠更好地控製作業生命週期,使我們能夠在自己的領域模型中啟用、停用或將作業標記為已完成。
正確的選擇取決於需求:如果我們只需要可靠的調度,那麼 Quartz 的持久性就足夠了;如果作業狀態是業務工作流程的一部分,那麼自訂儲存庫可能更合適。
與往常一樣,原始碼可在 GitHub 上取得。