Quartz 最佳实践

JobDataMap 小贴士

仅在 JobDataMap 中存储原始数据类型(包括字符串)

为了避免短期和长期的数据序列化问题,请仅在 JobDataMap 中存储原始数据类型(包括字符串)。

使用合并的 JobDataMap

在作业执行期间,JobExecutionContext上的 JobDataMap 提供了一种便利。JobDataMap 中的数据是 JobDetail 和 Trigger 的合并,Trigger 中的相同名称值会覆盖 JobDetail 中的值。

在某些情况下,在 Job 被调度器中存储以供常规/重复使用,并由多个 Trigger 触发时,在每次独立触发时向 Job 提供不同的数据输入,将 JobDataMap 值存储在 Trigger 上是有用的。

我们建议 IJob.Execute(..) 方法中的代码应从 JobExecutionContext 上的 MergedJobDataMap 检索值,而不是直接从 JobDetail 或 Trigger 检索。

public class SomeJob : IJob
{
    public static readonly JobKey Key = new JobKey("job-name", "group-name");

    public Task Execute(IJobExecutionContext context)
    {
        // 不要这样做
        var badMethod = context.JobDetail.JobDataMap.GetString("a-value");
        var alsoBadMethod = context.Trigger.JobDataMap.GetString("a-value");

        // 应该这样做
        var goodMethod = context.MergedJobDataMap.GetString("a-value");
    }
}

Job 小贴士

静态 Job Key

为了简化 JobKey 的访问,我们建议定义一个静态字段,方便地访问作业的键。

public class SomeJob : IJob
{
    public static readonly JobKey Key = new JobKey("job-name", "group-name");

    public Task Execute(IJobExecutionContext context)
    {
        // 省略执行逻辑
    }
}

之后,可以直接触发作业

public async Task DoSomething(IScheduler schedule, CancellationToken ct)
{
    await schedule.TriggerJob(SomeJob.Key, ct);
}

或使用 Trigger 进行调度

public async Task DoSomething(IScheduler schedule, CancellationToken ct)
{
    var trigger = TriggerBuilder.Create()
        .WithIdentity("a-trigger", "a-group")
        .ForJob(SomeJob.Key)
        .StartNow()
        .Build();

    await schedule.ScheduleJob(trigger, ct);
}

Trigger 小贴士

使用 TriggerUtils

TriggerUtils 提供:

  • 创建日期(用于开始/结束日期)的简便方法
  • 分析 Trigger 的助手函数(例如计算未来的触发时间)

使用 ScheduleJobs

当需要在调度器中使用大量作业时(例如,使用不同的 JobData 调用同一个作业),合理做法是调用 ScheduleJobs 方法,而不是在循环中触发作业或手动逐一调用:

Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>> jobsDictionary = new();
foreach (var data in allData)
{
    var triggerSet = new HashSet<ITrigger>();

    IJobDetail job = JobBuilder.Create<SomeJob>()
        .UsingJobData("jobData", data.ToString())
        .Build();

    ITrigger trigger = TriggerBuilder.Create()
        .ForJob(job)
        .Build();

    triggerSet.Add(trigger);
    jobsDictionary.Add(job, triggerSet);
}

await scheduler.ScheduleJobs(jobsDictionary, replace: true);

ADO.NET JobStore 小贴士

从不直接写入 Quartz 的表

直接通过 SQL 写入数据库(而不是使用调度 API)进行调度数据:

  • 导致数据损坏(删除的数据,混乱的数据)
  • 作业似乎在触发时间到达时“消失”而不执行
  • 作业不执行,“就在那里”当触发时间到达时
  • 可能导致:死锁
  • 其他奇怪的问题和数据损坏

从不在同一数据库中指向另一个具有相同调度器名称的非集群调度器

如果您让多个调度器实例指向同一套数据库表,并且其中一些实例未配置为集群,则可能发生以下任何情况:

  • 数据损坏(删除的数据,混乱的数据)
  • 作业在触发时间到达时似乎“消失”而不执行
  • 作业不执行,“就在那里”当触发时间到达时
  • 可能导致:死锁
  • 其他奇怪的问题和数据损坏

确保数据源连接大小充足

建议您的数据源最大连接数配置为线程池中工作线程数量加上三。如果您的应用还频繁调用调度器 API,您可能需要更多连接。

夏令时

避免在夏令时转换时段附近调度作业

注:转换小时和时间调整的具体情况因地区而异,请参阅:维基百科关于全球夏令时的页面

SimpleTrigger 不受夏令时影响,因为它们总是确切地在某个毫秒时间点触发,并且每隔确切的毫秒数重复。

由于 CronTrigger 在特定的小时/分钟/秒触发,当夏令时转换发生时,它们会遇到一些异常情况。

例如,在美国遵循夏令时的时区/地点,如果使用 CronTrigger 并在凌晨 1:00 至 2:00 之间安排触发时间,可能会出现以下问题:

  • 凌晨 1:05 可能触发两次!- CronTrigger 可能重复触发
  • 凌晨 2:05 可能永远不会触发!- CronTrigger 可能错过触发

同样,时间调整的具体情况和调整的量因地区而异。

其他基于滑动沿日历(而不是确切时间量)的触发类型,如 CalendarIntervalTrigger,也会受到影响——但不会错过触发或两次触发,而是可能最终其触发时间被推后一小时。

作业

等待条件

长时间运行的作业会阻止其他作业运行(如果线程池中的所有线程都很忙)。

如果您觉得需要在执行作业的工作线程上调用 Thread.sleep(),通常表示作业还没有准备好做其余的工作,因为它需要等待某些条件(如数据记录的可用性)变为真。

更好的解决方案是释放工作线程(退出作业),允许其他作业在该线程上执行。作业可以在退出之前重新调度自己或其他作业。

抛出异常

作业的 execute 方法应包含处理所有可能异常的 try-catch 块。

如果作业抛出异常,Quartz 通常会立即重新执行它,这意味着作业很可能会再次抛出相同的异常。这可能导致资源浪费,最坏的情况下,应用程序不稳定或崩溃。更好的做法是,作业捕获它可能遇到的所有异常,处理它们,并重新调度自己或其他作业来解决这个问题。

可恢复性和幂等性

标记为 “可恢复” 的进行中的作业在调度器失败后会自动重新执行。这意味着作业的一些 “工作” 将被执行两次。

这意味着作业应编码为幂等的,即无论执行多少次,其结果都是一样的。

监听器(TriggerListener, JobListener, SchedulerListener)

保持监听器代码简洁高效

不鼓励执行大量工作,因为原本用于执行作业(或完成 Trigger 并转而触发另一个作业等)的线程将被绑定在监听器中。

处理异常

每个监听器方法应包含处理所有可能异常的 try-catch 块。

如果监听器抛出异常,可能导致其他监听器不被通知,或阻止作业的执行等。

通过应用暴露调度器功能

注意安全性

一些用户通过应用程序界面暴露 Quartz 的调度器功能。这非常有用,但也极其危险。

确保不要错误地允许用户定义他们想要的任何类型的作业,以及他们想要的任何参数。例如,Quartz.Jobs 包中附带了一个现成的作业 NativeJob,它将执行定义为执行的任何任意本机(操作系统)系统命令。恶意用户可以使用此功能控制或破坏您的系统。

同样,如 SendEmailJob 等其他作业,几乎都可以被恶意利用。

允许用户定义他们想要的任何作业实质上打开了您的系统,使其面临各种 OWASP 和 MITRE 定义的命令注入攻击相当/等效的漏洞。

在本文档中