為什麼需要任務調度?
- 非即時處理:報表產生、資料同步、郵件發送、快取預熱、資料清理。
- 固定排程:每日 02:00 執行、每小時、每週一、Cron 表達式彈性排程。
- 避免使用者等待:前端觸發後台非同步任務,輪詢/WebSocket 推送結果。
Quartz 核心概念
| 概念 | 說明 |
Job | 任務介面,實作 execute(JobExecutionContext) |
JobDetail | 任務定義:名稱、群組、實作類別、持久化資料 |
Trigger | 觸發條件:SimpleTrigger(固定間隔)、CronTrigger(Cron 表達式) |
Scheduler | 核心調度器,管理 Job/Trigger 生命週期、啟動/暫停/叢集 |
JobStore | 持久化策略:RAMJobStore(記憶體)、JDBCJobStore(資料庫、支援叢集) |
Web 容器啟動自動啟動 Scheduler:Struts PlugIn 模式
struts-config.xml 註冊
<plug-in className="com.example.SchedulerPlugIn">
<set-property property="startOnLoad" value="true" />
<set-property property="startupDelay" value="5000" /> <!-- 延遲 5 秒,等資料源就緒 -->
</plug-in>
SchedulerPlugIn 實作
public class SchedulerPlugIn implements PlugIn {
private Scheduler scheduler;
private boolean startOnLoad = true;
private long startupDelay = 0;
public void setStartOnLoad(boolean b) { this.startOnLoad = b; }
public void setStartupDelay(long ms) { this.startupDelay = ms; }
@Override
public void init(ActionServlet servlet, ModuleConfig config) {
ServletContext ctx = servlet.getServletContext();
StdSchedulerFactory factory = (StdSchedulerFactory)
ctx.getAttribute(QuartzInitializerServlet.QUARTZ_FACTORY_KEY);
try {
scheduler = factory.getScheduler();
if (startOnLoad) {
Thread.sleep(startupDelay);
scheduler.start();
}
} catch (Exception e) { throw new RuntimeException(e); }
}
@Override
public void destroy() {
try { scheduler.shutdown(true); } catch (SchedulerException ignore) {}
}
}
QuartzInitializerServlet 於 web.xml 註冊(提供 StdSchedulerFactory)
<servlet>
<servlet-name>QuartzInitializer</servlet-name>
<servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class>
<init-param>
<param-name>config-file</param-name>
<param-value>/WEB-INF/quartz.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
定義 Job 與排程(程式碼式)
public class ReportJob implements Job {
@Override
public void execute(JobExecutionContext ctx) {
// 產生報表、寄送 Email...
}
}
// 在 ServletContextListener 或 PlugIn 中排程
JobDetail job = JobBuilder.newJob(ReportJob.class)
.withIdentity("reportJob", "reportGroup")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("reportTrigger", "reportGroup")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")) // 每日 02:00
.build();
scheduler.scheduleJob(job, trigger);
叢集模式關鍵設定(quartz.properties)
org.quartz.scheduler.instanceName = MyClusteredScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
org.quartz.dataSource.myDS.jndiURL = java:comp/env/jdbc/quartzDS
- 所有節點共用同一組資料庫表。
isClustered=true 啟用分散式鎖、負載均衡、故障轉移。
- 需建立 Quartz 官方提供的建表腳本(
tables_.sql)。
現代替代方案對比
| 方案 | 優點 | 缺點 | 適用場景 |
| Spring @Scheduled | 零配置、原生整合、支援 Cron/固定延遲 | 單機、無持久化、無叢集、無動態管理 | 簡單定時任務、微服務單節點 |
| Spring Batch | 大量資料處理、分塊、重試、重啟、監控 | 非通用排程、學習曲線高 | ETL、報表、資料遷移 |
| Quartz | 功能完整、叢集、持久化、動態管理、JTA 事務 | 配置複雜、API 老舊 | 企業級排程、需叢集/持久化 |
| xxl-job | 分散式、Web 控制台、動態分片、失敗重試、多語言 | 需部署調度中心、Java 生態外較弱 | 跨語言、可視化運維、大規模分散式 |
| PowerJob | 阿里開源、高效能、工作流、資料列血緣 | 社群較新 | 複雜任務編排、資料治理 |
最佳實務清單
- Job 類別實作
Serializable(叢集必備)。
- Job 內捕獲所有異常,記錄日誌,避免觸發器誤以為成功。
- 冪等性設計:同一任務重複執行不產生副作用(資料庫唯一鍵、狀態機)。
- 監控指標:任務執行時間、成功率、佇列堆積、執行緒池使用率。
- 優雅關閉:
scheduler.shutdown(true) 等待執行中任務完成。