我们经常遇到需要定时执行某些任务的情况,比如清理缓存、异步结果轮询等,如果不打算造轮子,那么选择一款合适的定时任务组件就很关键了。所幸,.Net 世界中的选项并不多:)
选型#
主要有以下四款:
Quartz.Net
:移植自 Java 生态的Quartz
,久经考验、成熟稳重,只是个人感觉有点过度设计,初次接触容易让人困惑;Coravel
:提供任务调度,缓存,排队,邮件,事件广播等功能,全面但不专一;Hangfire
:最大的特点是内置控制面板。分为社区版和商用版。github 上,这四款组件它的点赞数最多,可见其收欢迎程度。不过它更像一个定时任务管理解决方案,所涵盖的功能除任务本身,还涉及到账户管理、图表管理、告警系统等;FluentScheduler
:似乎是这四款中最易上手的,但是已经有段时间没更新了。
稳妥起见,项目前期建议选择坑少、专注、轻量、社区较为活跃的组件 Quartz.Net,再择机使用 Hangfire 打造一个任务管理中心。
如前所述,Quartz.Net 有过度设计的嫌疑,2.x 版本时期配置方式的杂乱可见一斑。虽然现在已经迭代到 3.x,一些概念还是需要花心思理解下。
主要概念#
我们围绕下面这个代码片段展开:
var schedulerFactory = builder.Services.GetRequiredService();
var scheduler = await schedulerFactory.GetScheduler();
// define the job and tie it to our HelloJob class
var job = JobBuilder.Create()
.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 策略处理已错过的那些触发点。