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

时间:2023-02-24 11:10:41

我们经常遇到需要定时执行某些任务的情况,比如清理缓存、异步结果轮询等,如果不打算造*,那么选择一款合适的定时任务组件就很关键了。所幸,.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详解