Quartz.Net 主要概念介紹和吐槽

萊布尼茨發表於2023-02-24

我們經常遇到需要定時執行某些任務的情況,比如清理快取、非同步結果輪詢等,如果不打算造輪子,那麼選擇一款合適的定時任務元件就很關鍵了。所幸,.Net 世界中的選項並不多:)

選型

主要有以下四款:

  • Quartz.Net:移植自 Java 生態的 Quartz,久經考驗、成熟穩重,只是個人感覺有點過度設計,初次接觸容易讓人困惑;
  • Coravel:提供任務排程,快取,排隊,郵件,事件廣播等功能,全面但不專一;
  • Hangfire:最大的特點是內建控制皮膚。分為社群版和商用版。github 上,這四款元件它的點贊數最多,可見其收歡迎程度。不過它更像一個定時任務管理解決方案,所涵蓋的功能除任務本身,還涉及到賬戶管理、圖表管理、告警系統等;
  • FluentScheduler:似乎是這四款中最易上手的,但是已經有段時間沒更新了。

穩妥起見,專案前期建議選擇坑少、專注、輕量、社群較為活躍的元件 Quartz.Net,再擇機使用 Hangfire 打造一個任務管理中心。

如前所述,Quartz.Net 有過度設計的嫌疑,2.x 版本時期配置方式的雜亂可見一斑。雖然現在已經迭代到 3.x,一些概念還是需要花心思理解下。

主要概念

我們圍繞下面這個程式碼片段展開:

var schedulerFactory = builder.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler();

// define the job and tie it to our HelloJob class
var job = JobBuilder.Create<HelloJob>()
    .WithIdentity("myJob", "group1")
    .UsingJobData("way", "email")
    .Build();

// Trigger the job to run now, and then every 40 seconds
var trigger = TriggerBuilder.Create()
    .WithIdentity("myTrigger", "group1")
    .StartNow()
    .WithSimpleSchedule(x => x
        .WithIntervalInSeconds(40)
        .RepeatForever())
    .Build();

await scheduler.ScheduleJob(job, trigger);

Jobs and Triggers

Quartz.Net 將任務業務邏輯(Jobs)和任務執行時間計劃(Triggers)做了分離。官方的解釋是兩者可以獨立管理,還可以多個 Trigger 觸發同一個 Job;有沒有必要見仁見智

Job 和 Trigger 還有group的概念,比如多個模組都有各自清理快取的任務,可以將這些任務劃分為同一個組。但是在實操過程中,group 除了語義上的指示,似乎並沒有直接的其它作用;也許它可以用於業務端的擴充套件,不過私以為元件不應將可能的業務需求作為自己的設計點,且業務擴充套件不應依賴某個具體元件

JobDetail

上述程式碼中變數 job 的型別並非IJob,而是IJobDetail,也就是說,Quartz.Net 排程維持的是 IJobDetail 例項,而 Job 例項,是在每次任務執行的時間點例項化的,執行完就銷燬,例項狀態並不能延續,需要藉助 IJobDetail 例項存取每次執行後更新的狀態。

JobDataMap

Job 例項狀態儲存在 JobDataMap 中,構造 JobDetail 物件時使用 .UsingJobData(key, value) 定義鍵值對。Trigger 也有 JobDataMap,是為上面提到的———多個 Trigger 觸發同一個 Job——這種情況設計的。

使用方式如下:

public class HelloJob : IJob
{
	public async Task Execute(IJobExecutionContext context)
	{
		JobKey key = context.JobDetail.Key;

                // 可使用 context.MergedJobDataMap 同時獲取 Trigger 提供的鍵值
		JobDataMap dataMap = context.JobDetail.JobDataMap;

		string way = dataMap.GetString("way");

		await Console.Error.WriteLineAsync("Instance " + key + " of HelloJob is send by: " + way);
	}
}

另外要注意的是,用來修飾 Job 類的特性DisallowConcurrentExecution,其實約束的是 JobDetail,即對於 JobDetail-A,同一時間,只能有一個 Job 使用它。也就是說,可以同時啟用多個 HelloJob 物件,只要它們關聯的 JobDetail 不同即可(當然了,實際我們也只能操作多個不同的 JobDetail 去啟用對應的 Job)。

關於 JobDetail 的解釋,官方文件寫得過於複雜,其實它的目的就是為了解決 Job 例項並非單例的問題,那麼作者為什麼不乾脆將 Job 例項單例化呢?根本沒必要創造一個多餘的 JobDetail 的概念,令人費解。如果是因為併發考量,那麼應該從 Job 定義入手,加入多執行緒支援。

如果專案中使用了 IOC,那麼我們可以選擇不使用 JobDataMap,而是將例項狀態儲存在自定義物件中,在每次例項化 Job 物件時注入。

Misfire 處理方案

當一個 Job 在規定時間點沒有被觸發執行(比如執行緒池裡面沒有可用的執行緒、Job 被暫停等),且超時時間超過 misfireThreshold 配置的值(預設為60秒),則作業會被排程程式認為Misfire

當系統恢復後(有空閒執行緒、Job 被恢復等),排程程式會根據配置的 Misfire 策略處理已錯過的那些觸發點。

參考資料

Quartz misfire詳解

相關文章