前言

排程任務在後端開發裡幾乎無所不在:定期備份資料、產生報表、發送郵件、執行系統維護。Quartz.NET 是 .NET 生態裡最成熟的開源排程框架,提供豐富的功能,也有足夠的彈性應對各種場景。

Scheduler Job Store Thread Pool Jobs Triggers Listeners Plugins

Quartz.NET 是什麼?

Quartz.NET 是 Java Quartz 排程框架的 .NET 移植版,提供企業級的排程功能:

  • 支援複雜的排程模式(Cron 表達式)
  • 具備叢集功能,可確保高可用性
  • 提供任務持久化
  • 彈性的錯誤處理機制
  • 豐富的監控與管理功能

基本概念

Quartz.NET 有三個核心元件:

  1. Job(工作):定義要執行的具體任務
  2. Trigger(觸發器):決定任務何時執行
  3. Scheduler(排程器):負責協調 Job 和 Trigger

實作範例

1. 安裝套件

使用 NuGet 安裝 Quartz:

1
Install-Package Quartz

2. 建立 Job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定義一個簡單的工作
public class HelloJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// 記錄工作執行時間
string executeTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

// 執行工作邏輯
await Console.Out.WriteLineAsync($"Hello! 執行時間: {executeTime}");

// 可以從 context 取得傳入的參數
var dataMap = context.JobDetail.JobDataMap;
string jobSays = dataMap.GetString("jobSays");
await Console.Out.WriteLineAsync($"參數值: {jobSays}");
}
}

3. 設定觸發器並啟動

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 建立排程器
IScheduler scheduler = await StdSchedulerFactory.GetDefaultScheduler();

// 定義工作
IJobDetail job = JobBuilder.Create<HelloJob>()
.WithIdentity("helloJob", "group1") // 設定工作名稱和群組
.UsingJobData("jobSays", "Hello World!") // 傳入參數
.Build();

// 建立觸發器 - 每 5 秒執行一次
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("trigger1", "group1") // 設定觸發器名稱和群組
.StartNow() // 立即開始
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(5) // 間隔 5 秒
.RepeatForever()) // 永遠重複
.Build();

// 註冊工作和觸發器
await scheduler.ScheduleJob(job, trigger);

// 啟動排程器
await scheduler.Start();

// 等待(Console 程式需要防止主執行緒提早結束)
await Task.Delay(TimeSpan.FromSeconds(60));

// 關閉排程器
await scheduler.Shutdown();

4. 使用 Cron 表達式

Cron 表達式適合需要精細排程的場景:

1
2
3
4
5
6
7
8
// 建立每天早上 9:00 執行的觸發器
ITrigger cronTrigger = TriggerBuilder.Create()
.WithIdentity("trigger2", "group1")
.WithCronSchedule("0 0 9 * * ?") // 每天早上 9:00
.Build();

// 替換掉原本的觸發器,重新排程
await scheduler.ScheduleJob(job, cronTrigger);

Cron 表達式格式(共 7 欄,年可省略):

欄位 範圍 範例
0–59 0
0–59 30
0–23 9
1–31 *
1–12 *
星期 1–7 ?
年(選填) 1970–2099

常用範例:

  • 0 0 9 * * ? — 每天早上 9:00 執行
  • 0 0/30 * * * ? — 每 30 分鐘執行一次
  • 0 0 9-17 * * ? — 每天 09:00–17:00,整點各執行一次

5. 錯誤處理

Quartz.NET 本身不會自動重試失敗的 Job,需要在 Execute 內自行處理。最常見的做法是捕捉例外、記錄日誌後,透過 JobExecutionException 告知排程器是否要立即重新執行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ErrorHandlingJob : IJob
{
private readonly ILogger<ErrorHandlingJob> _logger;

public ErrorHandlingJob(ILogger<ErrorHandlingJob> logger)
{
_logger = logger;
}

public async Task Execute(IJobExecutionContext context)
{
try
{
// 實際工作邏輯放這裡
await DoWorkAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Job {JobKey} 執行失敗", context.JobDetail.Key);

// refireImmediately = true 會讓 Quartz 立即重新排入佇列執行一次
var jobEx = new JobExecutionException(ex, refireImmediately: false);
throw jobEx;
}
}

private async Task DoWorkAsync()
{
// 實際業務邏輯
await Task.CompletedTask;
}
}

實務注意事項

Job 設計

  • 盡量讓每個 Job 只做一件事,避免在單次執行中混入太多業務邏輯
  • 長時間運行的 Job 記得處理 CancellationToken(透過 context.CancellationToken),讓排程器能安全中斷
  • 如果同一 Job 可能被多個執行緒同時觸發,加上 [DisallowConcurrentExecution] 屬性

執行緒池與頻率

  • 預設執行緒池大小為 10,高並行場景要視需求調整(quartz.threadPool.maxConcurrency
  • 過高頻率的排程(如每秒數次)適合考慮換成訊息佇列,避免 Job Store 成為瓶頸

日誌與可觀測性

  • Job 內建議注入 ILogger,用結構化日誌記錄執行時間與關鍵參數,方便事後追查
  • 生產環境可搭配 IJobListener 集中監聽所有 Job 的執行結果,不用每個 Job 各自重複寫

小結

這篇涵蓋了 Quartz.NET 最核心的用法:建 Job、設 Trigger、用 Cron 表達式,以及正確丟出 JobExecutionException。入門夠用了。進階方向包括持久化 Job Store(搭配資料庫)、叢集模式、以及 IJobListenerITriggerListener 做監控,官方文件有完整範例。

參考資源