Quartz 常见问题解答

一般性问题

Quartz 是什么?

Quartz 是一个作业调度系统,可与几乎任何其他软件系统集成或并行使用。"作业调度器"这一术语对不同人可能意味着不同的东西。阅读本教程时,你将能明确我们使用这个词的含义,简而言之,作业调度器是一个负责在预设定(计划)时间到达时执行(或通知)其他软件组件的系统。

Quartz 非常灵活,包含多种使用范式,可以单独或组合使用,以实现期望的行为,并允许你以对你项目来说最“自然”的方式编写代码。

Quartz 非常轻量级,需要很少的设置/配置——如果你的需求相对基本,它实际上可以“开箱即用”。

Quartz 具有容错性,可以在系统重启之间持久化(“记住”)你的预定作业。

尽管 Quartz 仅用于在给定时间表上运行某些系统进程就非常有用,但只有当你学会如何使用它来驱动应用程序业务流程的流动时,才能充分发挥 Quartz 的全部潜力。

从软件组件视角看 Quartz 是什么?

Quartz 作为一个包含所有核心功能的小型动态链接库(.dll 文件)发布。访问这些功能的主要接口是 Scheduler 接口,提供了如调度/取消调度作业、启动/停止/暂停调度器等简单操作。

如果你想安排自己的软件组件执行,它们必须实现简单的 Job 接口,该接口包含 execute() 方法。如果你希望在预定触发时间到达时通知组件,则组件应实现 TriggerListener 或 JobListener 接口。

Quartz 的主要“进程”可以在你自己的应用内启动和运行,或者作为一个带有远程接口的独立应用。

为什么不直接使用 System.Timers.Timer?

.NET Framework 提供了内置的计时器功能,通过 System.Timers.Timer 类实现 —— 为什么有人会使用 Quartz 而不是这些标准特性呢?

原因有很多!以下是一些理由:

  • 定时器没有持久化机制。
  • 定时器调度不灵活(只能设置开始时间和重复间隔,不能基于日期、时间等)。
  • 定时器不使用线程池(每个定时器一个线程)。
  • 定时器没有真正的管理方案 —— 你需要自己编写机制来记忆、组织和检索你的任务,比如按名称等。

当然,对于一些简单的应用来说,这些特性可能不重要,在这种情况下选择不使用 Quartz.NET 可能是正确的决定。

其他问题

Quartz 能够运行多少个作业?

这是一个难以回答的问题……答案基本上是“视情况而定”。

我知道你讨厌这个答案,所以这里提供一些关于其依赖因素的信息。

首先,你使用的 JobStore 对此有很大影响。基于 RAM 的 JobStore 比基于 ADO.NET 的 JobStore 快得多。AdoJobStore 的速度几乎完全取决于与数据库的连接速度、所使用的数据库系统以及数据库运行的硬件。实际上,Quartz 自身做的处理非常少,大部分时间都花在数据库上。当然,RAMJobStore 在可存储的作业和触发器数量上有一个更有限的限制,因为你肯定会有比数据库硬盘空间少的 RAM。你也可以查看 FAQ “如何提高 AdoJobStore 的性能?”

因此,Quartz 能够“存储”和监控的触发器和作业的数量限制实际上是 JobStore 可用的存储空间量(无论是 RAM 还是磁盘空间)。

现在,除了 “我能存储多少?” 之外,还有 “Quartz 同一时刻能运行多少作业?” 的问题。

一个可能会减慢 Quartz 本身的因素是使用大量的监听器(TriggerListeners、JobListeners 和 SchedulerListeners)。在每个监听器中花费的时间显然增加了 “处理” 作业执行的时间,超出了实际执行作业的时间。这并不意味着你应该害怕使用监听器,只是意味着你应该谨慎使用它们 —— 如果可以的话,不要创建大量“全局”监听器,而是创建更专业的监听器。此外,除非确实需要,否则不要在监听器中做 “昂贵” 的事情。还要注意,许多插件(如“历史记录”插件)实际上是监听器。

实际上同一时刻能够运行的作业数量受到线程池大小的限制。如果线程池中有五个线程,那么一次最多只能运行五个作业。但是要注意不要创建大量线程,因为虚拟机、操作系统和 CPU 都很难同时处理大量线程,仅仅由于所有的管理开销就会导致性能下降。在大多数情况下,当线程数达到数百时,性能开始变差。要记住,如果你在应用服务器内运行,它可能已经创建了至少几十个自己的线程!

除了这些因素外,真正归结于你的作业执行的是什么。如果你的作业完成工作需要很长时间,或者工作非常耗 CPU,那么显然你无法同时运行很多作业,也无法在特定时间段内运行很多作业。

最后,如果你不能从单个 Quartz 实例中获得足够的处理能力,你可以随时在多台机器上负载平衡多个 Quartz 实例。每一个实例都会根据触发器的需要,从共享数据库中按先到先服务的原则运行作业。

所以到这里,对于“多少”的问题,我仍然没有给出一个数字。而且我真的很不愿意给出,因为上面提到的所有变量。让我这样说吧,市面上安装的 Quartz Java 版本有的正在管理数十万的作业和触发器,而且在任何给定时刻都能执行数十个作业——这还不包括使用负载均衡。考虑到这一点,大多数人应该有信心从 Quartz 中获得他们所需的性能。

关于作业的问题

如何控制作业的实例化?

参见 Quartz.Spi.IJobFactoryQuartz.IScheduler.JobFactory 属性。

如何防止作业完成后被移除?

设置 JobDetail.Durable = true 属性,指示 Quartz 即使作业成为“孤儿”(当作业不再有触发器引用它时)也不删除作业。

如何防止作业并发执行?

Quartz.NET 2.x:实现 IJob 并且用 [DisallowConcurrentExecution] 特性装饰你的作业类。阅读 DisallowConcurrentExecutionAttribute 的 API 文档获取更多信息。

Quartz.NET 1.x:使作业类实现 IStatefulJob 而不是 IJob 。阅读 IStatefulJob 的 API 文档获取更多信息。

如何停止当前正在执行的作业?

Quartz 1.x 和 2.x:参见 Quartz.IInterruptableJob 接口和 IScheduler.Interrupt(string, string) 方法。

Quartz 3.x:参见 IJobExecutionContextCancellationToken.IsCancellationRequested

关于触发器的问题

如何链式执行作业?或者说,如何创建工作流?

目前 Quartz 没有 “直接” 或 “免费” 的方法来链接触发器。但是有几种方法可以不费太多力气就能实现。以下是几种方法的概述:

一种方法是使用监听器(例如 TriggerListener、JobListener 或 SchedulerListener),它能注意到作业/触发器的完成并立即调度新的触发器来触发。这种方法可能涉及较多细节,因为你需要告诉监听器哪个作业跟在哪个之后执行,并且可能需要考虑保存这些信息的持久化。

另一种方法是构建一个作业,其 JobDataMap 包含下一个要触发的作业的名称,并在作业完成(其 Execute() 方法的最后一步)时安排下一个作业。许多人正在这样做并且取得了很好的效果。大多数人都制作了一个知道如何使用特殊键(常量)从 JobDataMap 获取作业名和组的基础(抽象)类,并包含了调度标识的作业的代码。然后他们简单地扩展这个类,包含作业应做的额外工作。

将来,Quartz 将提供一种更干净的方式来做到这一点,但在那之前,你将不得不使用上述之一的方法,或者想出一个更适合你的方法。

为什么我的触发器没有触发?

最常见的原因是没有调用 Scheduler.Start() ,它告诉调度器开始触发触发器。

第二个最常见的原因是触发器或触发器组已被暂停。

夏令时与触发器

CronTrigger 和 SimpleTrigger 每个都以符合其触发器类型直观的方式处理夏令时。

首先,作为对夏令时是什么的回顾,请阅读此资源:夏令时概览。有些读者可能不知道不同国家/洲的规则是不同的。例如,2005 年的夏令时在美国于 4 月 3 日开始,但在埃及则是 4 月 29 日。同样重要的是要知道,不仅各地的日期不同,时间变化的时间也不同。许多地方在凌晨 2 点转换时间,但其他地方在凌晨 1 点、凌晨 3 点或午夜转换。

简单触发器(SimpleTrigger)允许你安排作业每隔 N 毫秒触发一次。因此,为了 "保持计划",它不需要特别针对夏令时做任何事情——它只是简单地每隔 N 毫秒触发一次。然而,这对一些用户来说可能有点混淆的地方在于,如果你的简单触发器每 12 小时触发一次,那么在夏令时切换前,它可能看似在凌晨 3 点和下午 3 点触发,但在夏令时后,它将在凌晨 4 点和下午 4 点触发。这不是一个错误。

  • 触发器确实一直在每隔 N 毫秒精确地触发,只是人类赋予那个时刻的 "时间名称" 发生了变化。

cron 触发器(CronTrigger) 允许你按照 "格里高利历" 来安排作业在特定时刻触发。因此,如果你创建了一个触发器,让它每天上午 10 点触发,在夏令时转换前后,它将继续这样做。然而,根据是春季还是秋季的夏令时调整,对于那个特定的周日来说,从周日早上 10 点触发到周六早上 10 点的触发,实际的时间间隔将不是 24 小时,而是分别变为 23 小时或 25 小时。

关于cron 触发器在夏令时上的一个额外要点是,你需要仔细考虑创建在午夜到凌晨 3 点之间(这个关键时间窗口取决于你的触发器所在的时区,如上所述)的调度。原因是,根据你的触发器的调度,以及特定的夏令时事件,触发器可能会被跳过,或者看起来有一两个小时没有触发。举个例子,假设你在美国,那里的夏令时事件发生在凌晨 2 点。如果你有一个 cron 触发器,每天凌晨 2 点 15 分触发,那么在夏令时开始的那天,触发器会被跳过,因为那天凌晨 2 点 15 分实际上不会出现。如果你有一个 cron 触发器,每天每小时的 15 分钟触发,那么在夏令时结束的那天,你会有一个小时的时间没有触发事件,因为当凌晨 2 点到来时,它会再次变成凌晨 1 点,然而 1 点整的触发都已经发生过了,触发器的下次触发时间已经被设置为凌晨 2 点—。

  • 因此接下来的一个小时内不会有任何触发

总之,所有这些都非常合理,只要你记住这两条规则,就应该容易理解:

  • 简单触发器总是准确地每隔 N 秒触发,与一天中的时间无关。
  • cron 触发器总是在给定的时间点触发,然后计算下一次触发的时间。如果那一天该时间点没有出现,触发器将会被跳过。如果在某一天该时间点出现了两次,它只会触发一次,因为在第一次触发后,它已经计算了下一次的触发时间。

关于 AdoJobStore 的问题

如何提高 AdoJobStore 的性能?

有几个已知的方法可以加快 AdoJobStore 的速度,但只有一个是非常实用的。

首先,显而易见但不太实用的:

  • 在运行 Quartz 和运行 RDBMS 的机器之间购买更快的网络。
  • 购买更强大的机器来运行你的数据库。
  • 购买更好的 RDBMS。

其次,使用特定于你的数据库的驱动代理实现,如 SQLServerDelegate,以获得最佳性能。

提示:你也应该始终优先使用最新版本的库。Quartz.NET 2.0 比 1.x 系列效率更高,而 2.2.x 系列又在之前的 2.x 版本基础上对 AdoJobStore 相关的性能进行了改进。

Quartz 在 Web 环境中的问题

调度器在应用程序池回收时不断停止

默认情况下,IIS 会定期回收和停止应用程序池。这意味着即使你在 Web 应用首次被访问时有 Application_Start 事件来启动 Quartz,调度器也可能因为站点不活跃而在稍后被处置。 如果你有 IIS 8 可用,你可以配置你的站点为预加载并保持运行状态。详情请参阅 这篇博客文章

在本文档中