Quartz.NET 配置参考

默认情况下,StdSchedulerFactory 会从 “当前工作目录” 加载名为 quartz.config 的属性文件。如果失败,那么将加载位于 Quartz DLL 中的(作为嵌入式资源的)quartz.config 文件。如果您希望使用这些默认文件之外的文件,必须定义系统属性 quartz.properties 来指向您想要的文件。

或者,您可以在调用 StdSchedulerFactory 上的 GetScheduler() 之前,通过调用 Initialize(xx) 方法之一来显式初始化工厂。

指定的 IJobStoreIThreadPool 和其他 SPI 类型的实例将按名称创建,然后配置文件中为它们指定的任何其他属性将通过调用等效的属性设置方法设置在实例上。例如,如果属性文件包含属性 quartz.jobStore.myProp = 10,那么在 JobStore 类型被实例化后,其 MyProp 属性的 setter 方法将被调用。对原始类型(int、long、float、double、boolean 和 string)的类型转换会在调用属性 setter 方法前进行。

一个属性可以通过指定遵循 $@other.property.name 约定的值来引用另一个属性的值,例如,要将调度器的实例名作为其他某个属性的值,您将使用 $@quartz.scheduler.instanceName

提示
您也可以使用基于代码的配置,这本质上是构建这些键的过程。

主配置

这些属性配置调度器的标识以及各种其他 “顶级” 设置。

属性名称 必需 类型 默认值
quartz.scheduler.instanceName string 'QuartzScheduler'
quartz.scheduler.instanceId string 'NON_CLUSTERED'
quartz.scheduler.instanceIdGenerator.type string Quartz.Simpl.SimpleInstanceIdGenerator, Quartz
quartz.scheduler.threadName string instanceName + '_QuartzSchedulerThread'
quartz.scheduler.makeSchedulerThreadDaemon boolean false
quartz.scheduler.idleWaitTime long 30000
quartz.scheduler.typeLoadHelper.type string Quartz.Simpl.SimpleTypeLoadHelper
quartz.scheduler.jobFactory.type string Quartz.Simpl.PropertySettingJobFactory
quartz.context.key.SOME_KEY string none
quartz.scheduler.wrapJobExecutionInUserTransaction boolean false
quartz.scheduler.batchTriggerAcquisitionMaxCount int 1
quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow long 0

quartz.scheduler.instanceName

可以是任何字符串,该值对调度器本身没有意义,但作为客户端代码在同一个程序中区分多个调度器的机制。如果您正在使用集群功能,集群中每个“逻辑”上相同的调度器实例都必须使用相同的名称。

quartz.scheduler.instanceId

可以是任何字符串,但对于集群中作为同一个“逻辑”调度器工作的所有调度器来说,必须是唯一的。您可以使用值"AUTO"作为 instanceId,希望为您生成 Id。或者使用"SYS_PROP",如果您希望值来自系统属性"quartz.scheduler.instanceId"。

quartz.scheduler.instanceIdGenerator.type

仅在 quartz.scheduler.instanceId 设置为"AUTO"时使用。默认为"Quartz.Simpl.SimpleInstanceIdGenerator",它基于主机名和时间戳生成实例 ID。其他 InstanceIdGenerator 实现包括 SystemPropertyInstanceIdGenerator(从系统属性"quartz.scheduler.instanceId"获取实例 ID)和 HostnameInstanceIdGenerator(使用本地主机名)。您也可以自己实现 InstanceIdGenerator 接口。

quartz.scheduler.threadName

可以是任何字符串,作为调度器主线程的有效名称。如果未指定此属性,线程将接收调度器的名称("quartz.scheduler.instanceName")加上附加的字符串'_QuartzSchedulerThread'。

quartz.scheduler.makeSchedulerThreadDaemon

布尔值('true'或'false'),指定调度器主线程是否应为守护线程。有关调整的信息,请参阅 quartz.scheduler.makeSchedulerThreadDaemon 属性,如果您使用的线程池实现是 DefaultThreadPool(这很可能是大多数情况)。

quartz.scheduler.idleWaitTime

是在调度器空闲时重新查询可用触发器之前等待的时间量(以毫秒为单位)。通常,您不需要“调整”此参数,除非您正在使用 XA 事务,并且遇到了应该立即触发的触发器延迟触发的问题。不建议使用小于 5000ms 的值,因为它会导致数据库查询过度。小于 1000 的值是不合法的。

quartz.scheduler.typeLoadHelper.type

默认为最健壮的方法,即使用"Quartz.Simpl.SimpleTypeLoadHelper"类型——它只是使用 Type.GetType() 进行加载。

quartz.scheduler.jobFactory.type

要使用的 IJobFactory 的类型名称。作业工厂负责生成 IJob 实现的实例。默认是'Quartz.Simpl.PropertySettingJobFactory',它每次执行即将发生时简单地调用 Activator.CreateInstance 给出的类型来产生一个新的实例。 PropertySettingJobFactory 还反射性地使用调度器上下文和作业及触发器的 JobDataMaps 的内容设置作业的属性。

quartz.context.key.SOME_KEY

表示将被放入“调度器上下文”中的键值对(参见 IScheduler.Context)。因此,例如,设置"quartz.context.key.MyKey = MyValue"将执行相当于 scheduler.Context.Put("MyKey", "MyValue") 的操作。

quartz.scheduler.batchTriggerAcquisitionMaxCount

允许调度器节点一次获取(以便触发)的最大触发器数量。默认值为 1。数值越大,在需要同时触发大量触发器的情况下效率越高——但代价是可能在集群节点之间负载不平衡。

如果此属性的值设置为> 1,并且使用了 AdoJobStore,则属性 "quartz.jobStore.acquireTriggersWithinLock" 必须设置为 "true" 以避免数据损坏。

quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow

触发器允许被提前获取并触发的时间量(以毫秒为单位)。默认值为 0。数值越大,批量获取触发器以进行触发就越有可能一次选择并触发多于一个触发器——代价是触发器的调度可能不会被精确遵守(触发可能会提前这个时间段)。

在调度器有大量触发器需要几乎同时或接近同时触发的情况下,这可能对于性能优化是有用的。

线程池

属性名称 必需 类型 默认值
quartz.threadPool.type string Quartz.Simpl.DefaultThreadPool
quartz.threadPool.maxConcurrency int 10

quartz.threadPool.type

这是您希望使用的线程池实现的名称。随 Quartz 一起提供的线程池是 "Quartz.Simpl.DefaultThreadPool",应该能满足几乎所有用户的需求。

它具有非常简单的行为并且经过了充分测试。它将任务分发到.NET 任务队列,并确保遵守配置的最大并发任务数限制。如果您想在 CLR 级别上微调线程池,应该研究 CLR 托管线程池

quartz.threadPool.maxConcurrency

这是可以分发到 CLR 线程池的并发任务数。如果您只有几个一天触发几次的工作,那么 1 个任务就足够了!如果有成千上万的工作,每分钟有很多都在触发,那么您可能希望最大并发计数更像是 50 或 100(这高度依赖于您的工作任务性质以及系统资源!)。另外请注意,CLR 线程池配置独立于 Quartz 本身。

自定义线程池

如果您使用自己的线程池实现,只需按照如下命名属性,就可以通过反射方式设置属性:

自定义线程池设置属性

quartz.threadPool.type = MyLibrary.FooThreadPool, MyLibrary
quartz.threadPool.somePropOfFooThreadPool = someValue

监听器

全局监听器可以通过 StdSchedulerFactory 实例化和配置,或者您的应用程序可以在运行时自己做这件事,然后将监听器注册到调度器上。“全局”监听器会监听每个作业/触发器的事件,而不仅仅是直接引用它们的作业/触发器。

通过配置文件配置监听器包括给它们命名,然后指定类型名称以及要设置在实例上的任何其他属性。类型必须有一个无参构造函数,属性通过反射设置。只支持原始数据类型值(包括字符串)。

因此,定义一个 “全局” TriggerListener 的一般模式是:

配置全局 TriggerListener

quartz.triggerListener.NAME.type = MyLibrary.MyListenerType, MyLibrary
quartz.triggerListener.NAME.propName = propValue
quartz.triggerListener.NAME.prop2Name = prop2Value

定义一个 “全局” JobListener 的一般模式是:

配置全局 JobListener

quartz.jobListener.NAME.type = MyLibrary.MyListenerType, MyLibrary
quartz.jobListener.NAME.propName = propValue
quartz.jobListener.NAME.prop2Name = prop2Value

插件

与监听器类似,通过配置文件配置插件也包括给它们命名,然后指定类型名称以及要设置在实例上的任何其他属性。类型必须有一个无参构造函数,属性通过反射设置。只支持原始数据类型值(包括字符串)。

因此,定义插件的一般模式是:

配置插件

Quartz 附带了几种插件,可以在 Quartz.Plugins 包中找到。以下是配置其中一些插件的示例:

日志触发器历史记录插件的示例配置

日志触发器历史记录插件捕获触发器事件(它也是一个触发器监听器)并使用日志基础架构记录这些事件。

日志触发器历史记录插件的示例配置

quartz.plugin.triggHistory.type = Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins
quartz.plugin.triggHistory.triggerFiredMessage = Trigger {1}.{0} fired job {6}.{5} at: {4:HH:mm:ss MM/dd/yyyy}
quartz.plugin.triggHistory.triggerCompleteMessage = Trigger {1}.{0} completed firing job {6}.{5} at {4:HH:mm:ss MM/dd/yyyy} with resulting trigger instruction code: {9}

XML 调度数据处理器插件的示例配置

作业初始化插件从 XML 文件中读取一组作业和触发器,并在初始化期间将它们添加到调度器中。它还可以删除现有数据。

JobInitializationPlugin 的示例配置

该文件的 XML 模式定义可在此处 找到

关闭钩子插件的示例配置

关闭钩子插件捕获 CLR 终止事件,并调用调度器的关闭方法。

ShutdownHookPlugin 的示例配置

quartz.plugin.shutdownhook.type = Quartz.Plugin.Management.ShutdownHookPlugin, Quartz.Plugins
quartz.plugin.shutdownhook.cleanShutdown = true

作业中断监控插件的示例配置

此插件捕获作业运行时间过长(超过配置的最大时间)的事件,并在启用时告诉调度器尝试中断它。插件默认在 5 分钟后发出中断信号,但默认值可以在配置中更改为不同的值,单位为毫秒。

JobInterruptMonitorPlugin 的示例配置

quartz.plugin.jobAutoInterrupt.type = Quartz.Plugin.Interrupt.JobInterruptMonitorPlugin, Quartz.Plugins
quartz.plugin.jobAutoInterrupt.defaultMaxRunTime = 3000000

远程服务器和客户端

属性名称 必需 类型 默认值
quartz.scheduler.exporter.type string
quartz.scheduler.exporter.port int
quartz.scheduler.exporter.bindName string 'QuartzScheduler'
quartz.scheduler.exporter.channelType string 'tcp'
quartz.scheduler.exporter.channelName string 'http'
quartz.scheduler.exporter.typeFilterLevel string 'Full'
quartz.scheduler.exporter.rejectRemoteRequests boolean false

如果您希望 Quartz 调度器通过远程处理作为服务器导出自身,那么请将 'quartz.scheduler.exporter.type' 设置为 "Quartz.Simpl.RemotingSchedulerExporter, Quartz" 。

quartz.scheduler.exporter.type

ISchedulerExporter 的类型,当前仅支持 "Quartz.Simpl.RemotingSchedulerExporter, Quartz" 。

quartz.scheduler.exporter.port

监听的端口。

quartz.scheduler.exporter.bindName

绑定到远程基础设施时使用的名称。

quartz.scheduler.exporter.channelType

'tcp' 或 'http'之一,TCP 性能更高。

quartz.scheduler.exporter.channelName

绑定到远程基础设施时使用的通道名称。

quartz.scheduler.exporter.typeFilterLevel

.NET Framework 远程处理的低反序列化级别。它支持与基本远程功能相关的类型。

.NET Framework 远程处理的完全反序列化级别。它支持远程处理在所有情况下支持的所有类型。

quartz.scheduler.exporter.rejectRemoteRequests

布尔值(true 或 false),指定是否拒绝来自其他计算机的请求。指定 true 仅允许来自本地计算机的远程调用。

RAMJobStore

RAMJobStore 用于在内存中存储调度信息(作业、触发器和日历)。RAMJobStore 快速且轻量级,但在进程终止时会丢失所有调度信息。

通过设置 quartz.jobStore.type 属性来选择 RAMJobStore: 将调度器的 JobStore 设置为 RAMJobStore

quartz.jobStore.type = Quartz.Simpl.RAMJobStore, Quartz

RAMJobStore 可通过以下属性进行调整:

属性名称 必需 类型 默认值
quartz.jobStore.misfireThreshold int 60000

quartz.jobStore.misfireThreshold

调度器将容忍触发器在其下次触发时间后错过多少毫秒,之后被视为“错失”。默认值(如果您不在配置中为此属性输入条目)是 60000(60 秒)。

JobStoreTX(ADO.NET)

AdoJobStore 用于在关系数据库中存储调度信息(作业、触发器和日历)。实际上有两个单独的 AdoJobStore 实现供您选择,具体取决于您需要的事务行为。

JobStoreTX 通过在每次操作(如添加作业)后调用 Commit()(或 Rollback() )来管理所有事务。除非您希望集成到某些事务感知框架,否则这通常是您应该使用的作业存储。

通过设置 quartz.jobStore.type 属性来选择 JobStoreTX:

将调度器的 JobStore 设置为 JobStoreTX

quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz

JobStoreTX 可通过以下属性进行调整:

属性名称 必需 类型 默认值
quartz.jobStore.dbRetryInterval long 15000(15 秒)
quartz.jobStore.driverDelegateType string null
quartz.jobStore.dataSource string null
quartz.jobStore.tablePrefix string "QRTZ_"
quartz.jobStore.useProperties boolean false
quartz.jobStore.misfireThreshold int 60000
quartz.jobStore.clustered boolean false
quartz.jobStore.clusterCheckinInterval long 15000
quartz.jobStore.maxMisfiresToHandleAtATime int 20
quartz.jobStore.selectWithLockSQL string SELECT \* FROM {0}LOCKS WHERE SCHED_NAME = {1} AND LOCK_NAME = ? FOR UPDATE
quartz.jobStore.txIsolationLevelSerializable boolean false
quartz.jobStore.acquireTriggersWithinLock boolean false(或 true - 见下文文档)
quartz.jobStore.lockHandler.type string null
quartz.jobStore.driverDelegateInitString string null

quartz.jobStore.dbRetryInterval

当调度器检测到 JobStore(例如,到数据库)中连接丢失时,它将在再次尝试之间等待的毫秒数。当使用 RamJobStore 时,此参数显然没有太大的意义。

quartz.jobStore.driverDelegateType

驱动代理理解不同数据库系统的特定“方言”。可能的内置选项包括:

  • Quartz.Impl.AdoJobStore.StdAdoDelegate, Quartz - 当没有特定实现可用时的默认值
  • Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz - 用于 Microsoft SQL Server
  • Quartz.Impl.AdoJobStore.PostgreSQLDelegate, Quartz
  • Quartz.Impl.AdoJobStore.OracleDelegate, Quartz
  • Quartz.Impl.AdoJobStore.SQLiteDelegate, Quartz
  • Quartz.Impl.AdoJobStore.MySQLDelegate, Quartz

quartz.jobStore.dataSource

此属性的值必须是配置属性文件中定义的数据源之一的名称。

quartz.jobStore.tablePrefix

AdoJobStore 的 “表前缀” 属性是一个字符串,等于在您的数据库中创建的 Quartz 表的前缀。如果您使用不同的表前缀,可以在同一数据库中拥有 Quartz 表的多组。

在 tablePrefix 中包含架构名

对于支持架构的后台数据库(如 Microsoft SQL Server),您可以使用 tablePrefix 包含架构名。例如,对于名为 foo 的架构,前缀可以设置为:

[foo].QRTZ_

注意: 如果使用显式架构(如 dbo)运行的任何数据库表创建脚本,都需要修改以反映此配置。

quartz.jobStore.useProperties

"使用属性" 标志指示 AdoJobStore 所有 JobDataMaps 中的值都将是字符串,因此可以作为名称-值对存储,而不是以序列化的形式存储更复杂的对象在 BLOB 列中。这很方便,因为您避免了非字符串类型序列化到 BLOB 时可能出现的版本控制问题。

quartz.jobStore.clustered

设置为 "true" 以开启集群特性。如果多个 Quartz 实例使用相同的数据库表集,则此属性必须设置为"true",否则您将会遇到混乱。有关集群的更多信息,请参阅配置文档。

quartz.jobStore.clusterCheckinInterval

设置此实例与其他集群实例"签到"的频率(以毫秒为单位)。影响检测失败实例的速度。

quartz.jobStore.maxMisfiresToHandleAtATime

job store 在给定的处理周期内将处理的最大错失触发器数量。一次处理很多(超过几十个)可能会导致数据库表被锁定的时间过长,从而影响尚未错失的触发器的触发性能。

quartz.jobStore.selectWithLockSQL

必须是一个 SQL 字符串,该字符串在 "LOCKS" 表中选择一行并锁定该行。如果不设置,默认为 SELECT * FROM {0}LOCKS WHERE SCHED_NAME = {1} AND LOCK_NAME = ? FOR UPDATE ,这适用于大多数数据库。"{0}" 在运行时被替换为您上面配置的 TABLE_PREFIX。"{1}" 被替换为调度器的名称。

quartz.jobStore.txIsolationLevelSerializable

"true" 的值告诉 Quartz(在使用 JobStoreTX 或 CMT 时)将 ADO.NET 连接的事务级别设置为串行化。这对于防止高负载下某些数据库的锁超时以及"长时间持续"的事务很有帮助。

quartz.jobStore.acquireTriggersWithinLock

无论是在显式的数据库锁内获取下一个要触发的触发器。这在过去版本的 Quartz 中曾经是必要的,以避免特定数据库的死锁,但现在不再认为是必要的,因此默认值为 "false"。

如果 "quartz.scheduler.batchTriggerAcquisitionMaxCount" 设置为大于 1,并且使用 AdoJobStore,则此属性必须设置为 "true" 以避免数据损坏(自 Quartz 2 起,如果 batchTriggerAcquisitionMaxCount 设置为大于 1,则默认值为 "true")。

quartz.jobStore.lockHandler.type

用于生成 Quartz.Impl.AdoJobStore.ISemaphore 实例的类型名称,用于对 job store 数据进行锁定控制。这是一个高级配置特性,大多数用户不应使用。

默认情况下,Quartz 会选择最合适的(预先捆绑的)Semaphore 实现来使用。

自定义 StdRowLockSemaphore

如果您明确选择使用此 DB Semaphore,您可以进一步定制如何频繁地轮询数据库锁。

使用自定义 StdRowLockSemaphore 实现的例子

quartz.jobStore.lockHandler.type = Quartz.Impl.AdoJobStore.StdRowLockSemaphore
quartz.jobStore.lockHandler.maxRetry = 7 # 默认是 3
quartz.jobStore.lockHandler.retryPeriod = 3000 # 默认是 1000 毫秒

quartz.jobStore.driverDelegateInitString

由管道符号分隔的属性(及其值)列表,可以在初始化时传递给 DriverDelegate。

字符串的格式如下:

settingName=settingValue|otherSettingName=otherSettingValue|...

StdAdoDelegate 及其所有后代(Quartz 附带的所有委托)支持一个名为 'triggerPersistenceDelegateTypes' 的属性,它可以设置为实现了 ITriggerPersistenceDelegate 接口的类型的逗号分隔列表,用于存储自定义触发器类型。参见 SimplePropertiesTriggerPersistenceDelegateSupportSimplePropertiesTriggerPersistenceDelegateSupport 的实现,了解为自定义触发器编写持久化代理的示例。

数据源(ADO.NET JobStores)

如果您使用 AdoJobstore,您将需要为其使用数据源(如果使用 JobStoreCMT,则需要两个数据源)。

您定义的每个数据源(通常是一个或两个)都必须被赋予一个名称,您为每个数据源定义的属性必须包含该名称,如下所示。数据源的"NAME"可以是您想要的任何内容,除了用于分配给 AdoJobStore 时能够识别它之外没有任何含义。

Quartz 创建的数据源定义了以下属性:

属性名称 必需 类型 默认值
quartz.dataSource.NAME.provider string
quartz.dataSource.NAME.connectionString string
quartz.dataSource.NAME.connectionStringName string
quartz.dataSource.NAME.connectionProvider.type string

quartz.dataSource.NAME.provider

目前支持以下数据库提供者:

  • SqlServer - Microsoft SQL Server
  • OracleODP - Oracle 的 Oracle 驱动
  • OracleODPManaged - 针对 Oracle 11 的 Oracle 托管驱动
  • MySql - MySQL Connector/.NET
  • SQLite - SQLite ADO.NET Provider
  • SQLite-Microsoft - Microsoft SQLite ADO.NET Provider
  • Firebird - Firebird ADO.NET Provider
  • Npgsql - PostgreSQL Npgsql

quartz.dataSource.NAME.connectionString

使用的 ADO.NET 连接字符串。如果您使用 connectionStringName,则可以跳过此步骤。

quartz.dataSource.NAME.connectionStringName

使用的连接字符串名称。在 app.config 或 appsettings.json 中定义。

quartz.dataSource.NAME.connectionProvider.type

允许您定义实现 IDbProvider 接口的自定义连接提供程序。

Quartz 定义的数据源示例

quartz.dataSource.myDS.provider = SqlServer
quartz.dataSource.myDS.connectionString = Server=localhost;Database=quartznet;User Id=quartznet;Password=quartznet;

集群

Quartz 的集群特性通过故障转移和负载均衡功能为您的调度器带来了高可用性和可扩展性。

集群目前仅适用于 AdoJobstore( JobStoreTXJobStoreCMT ),本质上是通过让集群中的每个节点共享同一个数据库来工作。

负载均衡自动发生,集群中的每个节点尽可能快地触发作业。当触发器的触发时间到达时,第一个获取它的节点(通过在它上放置锁)就是将触发它的节点。

每个触发只会触发一次作业。我的意思是,如果作业有重复触发器告诉它每 10 秒触发一次,那么正好在 12:00:00 只有一个节点会运行作业,在 12:00:10 也只有正好一个节点会运行作业,以此类推。它不一定是同一个节点每次都会执行 - 哪个节点执行会或多或少是随机的。负载平衡机制对于繁忙的调度器(许多触发器)来说几乎是随机的,但对于非繁忙(例如,少数触发器)的调度器则倾向于使用同一个节点。

故障转移发生在某个节点在执行一个或多个作业的过程中失败时。当一个节点失败时,其他节点检测到这个情况并识别出数据库中在失败节点中处于进行状态的作业。任何标记为恢复的作业(在 JobDetail 上的"requests recovery"属性)将由剩余节点重新执行。未标记为恢复的作业将简单地释放,以便在下次相关触发器触发时执行。

集群特性最适合扩展长期运行和/或 CPU 密集型作业(在多个节点上分配工作负载)。如果您需要扩展以支持数千个短时运行(例如 1 秒)的作业,请考虑通过使用多个独立的调度器(包括多个集群调度器以实现 HA)来对作业集进行分区。调度器使用集群范围内的锁,当您添加更多节点(超过大约三个节点 - 取决于您的数据库的能力等)时,性能会下降。

通过设置 quartz.jobStore.clustered 属性为 "true" 来启用集群。集群中的每个实例应使用相同的 quartz.properties 文件副本。例外情况可能是使用相同的属性文件,允许以下例外:不同的线程池大小,以及 quartz.scheduler.instanceId 属性的不同值。集群中的每个节点都必须有一个唯一的 instanceId,这很容易做到(无需不同的属性文件),只需将"AUTO"作为此属性的值即可。有关 AdoJobStore 配置属性的更多信息,请参阅配置文档。

危险
除非它们的时钟使用某种形式的时间同步服务(守护进程)定期运行(彼此的时钟必须在一秒之内)同步,否则绝不要在单独的机器上运行集群。如果您不熟悉如何执行此操作,请参阅 NIST 时间频率部门提供的互联网时间服务
危险
绝不要启动( scheduler.Start() )一个非集群实例针对任何其他实例正在运行( Start() )的相同数据库表集。您可能会遇到严重的数据损坏,并且肯定会经历不稳定的性能。
危险
监控并确保您的节点有足够的 CPU 资源来完成作业。当某些节点达到 100%的 CPU 使用率时,它们可能无法更新它们的触发器状态,从而可能导致其他节点尝试执行相同的作业,这可能会导致作业多次执行。
在本文档中