Quartz.NET RecurrenceTrigger 完全指南:用 RFC 5545 RRULE 实现 Cron 无法表达的复杂调度

发布时间:2026/10/7 9:54:37
Quartz.NET RecurrenceTrigger 完全指南:用 RFC 5545 RRULE 实现 Cron 无法表达的复杂调度 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载导读RecurrenceTrigger 是 Quartz.NET 从 3.18 版本起提供的新触发器族见 quartz-3.x 教程它以 iCalendar RFC 5545 的重复规则RRULE为调度描述语言专门解决 CronTrigger 与 SimpleTrigger 难以表达的周期模式——例如每月第二个星期一每隔一周的周一、周三、周五每年三月最后一个工作日。本文将系统讲解 RRULE 语法与各属性语义、六类典型实战示例、时区处理、DI 托管配置、misfire 策略并结合仓库源码剖析其惰性计算原理与 ADO.NET JobStore 持久化细节帮助你准确评估何时用 RecurrenceTrigger、何时继续用其他触发器族。为什么需要 RecurrenceTrigger现有触发器的表达力边界Quartz.NET 传统的调度能力分布如下SimpleTrigger固定时间间隔如每 10 秒一次无法表达按周几或按月某天这种日历语义CronTrigger用 6~7 段 Cron 表达式描述周期但面对某月的第几个星期几每两周的某几天一年中最后一个工作日这类序数ordinal与集合过滤组合时表达式要么极其别扭要么根本无法表达CalendarIntervalTrigger以每 N 个月 / N 周 / N 年为步长的日历间隔同样不关心星期几或日序。RecurrenceTrigger 把这些缺口用标准 RRULE 补上一条 RRULE 字符串即可描述FREQMONTHLY;BYDAY2MO每月第二个周一且触发时间按需惰性计算——不会一次性物化所有发生时刻天然适配长时间运行的调度任务。从源码看该触发器族由 IRecurrenceTrigger 接口定义暴露RecurrenceRule、TimeZone、TimesTriggered、MisfireInstruction四个只读成员其核心解析与计算引擎位于 RecurrenceRule频率枚举 RecurrenceFrequency 按SECONDLY → YEARLY从细到粗排序该顺序被ByRuleExpander用来判断 BY* 规则是扩展还是收窄结果集RFC 5545 3.3.10 节语义。RRULE 基础语法与属性详解一条 RRULE 是由分号分隔的键值对序列例如FREQMONTHLY;INTERVAL3;BYMONTHDAY1,15;COUNT10。其中FREQ为必填的基础频率其余属性用于收窄或细化模式。各属性语义见下表属性说明示例FREQ基础频率必填YEARLY、MONTHLY、WEEKLY、DAILY、HOURLY、MINUTELY、SECONDLYINTERVAL重复间隔默认 1INTERVAL2每隔一个周期COUNT触发最大次数COUNT10UNTIL重复的结束时间UNTIL20251231T235959ZBYDAY星期几可带序数前缀BYDAYMO,WE,FR或BYDAY2MO第二个周一BYMONTHDAY月内日1~31 或 -1~-31BYMONTHDAY15或BYMONTHDAY-1月末最后一天BYMONTH年内月份1~12BYMONTH1,6,12BYSETPOS在展开集合中的位置BYSETPOS-1最后一个发生点BYHOUR小时0~23BYHOUR9,17BYMINUTE分钟0~59BYMINUTE0,30BYSECOND秒0~59BYSECOND0BYWEEKNO周序号1~53 或 -53~-1BYWEEKNO1,26BYYEARDAY年内日序1~366 或 -366~-1BYYEARDAY1,100,200WKST一周起始日默认MOWKSTSU解析规则的源码级约束对照 RecurrenceRule.Parse 的实现可以确认以下硬性校验FREQ 必填缺失时抛出FormatExceptionRRULE must contain a FREQ propertyCOUNT 与 UNTIL 互斥同时出现会抛出FormatException两者都未设置时规则没有自然终点是否停火取决于触发器的 EndTimeINTERVAL 必须 ≥ 1、COUNT 必须 ≥ 1各 BY* 列表有取值范围校验如BYMONTHDAY为 -31~31 且不允许 0、BYHOUR为 0~23、BYMINUTE/BYSECOND为 0~59、BYWEEKNO为 -53~53、BYYEARDAY/BYSETPOS为 -366~366 且不允许 0未知属性会被静默忽略——这是对 RFC 5545 中 X- 扩展属性的兼容处理意味着拼写错误可能不会被立即发现构建时请仔细核对键名。此外RRULE:前缀是可选的字符串以该前缀开头时会被自动剥离不区分大小写。RecurrenceScheduleBuilder的构造函数会立即调用RecurrenceRule.Parse做 fail-fast 校验与CronScheduleBuilder的行为保持一致见 RecurrenceScheduleBuilder.cs。COUNT 计数的特殊语义重要与严格 RFC 5545 的按理论发生次数计数不同Quartz.NET 的COUNT统计的是实际触发次数TimesTriggered因 misfire 策略如DoNothing而被跳过的漏触发不计入COUNT因 misfire 策略如FireOnceNow而立即补的一次触发会计入COUNT。这遵循 Quartz.NET 通用的触发器语义。从实现上也能印证GetFireTimeAfter先检查TimesTriggered rule.Count便返回 nullRecurrenceTriggerImpl.csCOUNT 由触发器外部统一记账而非由 RRULE 引擎内部从起点重数一遍。实战示例六类典型 RRULE以下示例均取自可编译的示例工程 RecurrenceTriggerSamples.cs与文档中的用法一一对应。每月第二个星期一上午 9:00ITrigger trigger TriggerBuilder.Create() .WithIdentity(monthlyTrigger, group1) .WithRecurrenceSchedule(FREQMONTHLY;BYDAY2MO) .StartAt(DateBuilder.Create().InYear(2025).InMonthOnDay(1, 1).AtHourMinuteAndSecond(9, 0, 0).Build()) .Build();这里用StartAt指定 2025 年 1 月 1 日 9:00 作为 DTSTART即规则的起点时间。每隔一周的周一、周三、周五ITrigger trigger TriggerBuilder.Create() .WithIdentity(weeklyTrigger, group1) .WithRecurrenceSchedule(FREQWEEKLY;INTERVAL2;BYDAYMO,WE,FR) .StartNow() .Build();每年三月最后一个工作日BYSETPOS 组合ITrigger trigger TriggerBuilder.Create() .WithIdentity(yearlyTrigger, group1) .WithRecurrenceSchedule(FREQYEARLY;BYMONTH3;BYDAYMO,TU,WE,TH,FR;BYSETPOS-1) .StartNow() .Build();解析逻辑先按BYMONTH3、BYDAYMO,TU,WE,TH,FR展开三月的全部工作日再用BYSETPOS-1取展开集合中的最后一个。这是每个月/年最后一个 X模式的通用写法。每天但仅工作日跳过周末ITrigger trigger TriggerBuilder.Create() .WithIdentity(weekdayTrigger, group1) .WithRecurrenceSchedule(FREQDAILY;BYDAYMO,TU,WE,TH,FR) .StartNow() .Build();每月最后一天ITrigger trigger TriggerBuilder.Create() .WithIdentity(lastDayTrigger, group1) .WithRecurrenceSchedule(FREQMONTHLY;BYMONTHDAY-1) .StartNow() .Build();负数日序从月末反向计数-1即该月最后一天28/29/30/31 自动适配。每 3 个月的 1 日和 15 日最多 10 次ITrigger trigger TriggerBuilder.Create() .WithIdentity(quarterlyTrigger, group1) .WithRecurrenceSchedule(FREQMONTHLY;INTERVAL3;BYMONTHDAY1,15;COUNT10) .StartNow() .Build();注意此处的COUNT10按上述语义统计实际触发次数而非理论发生数。时区支持InTimeZone 的用法与默认行为Recurrence 计算默认使用系统本地时区。如需指定时区在WithRecurrenceSchedule的配置委托中调用InTimeZoneITrigger trigger TriggerBuilder.Create() .WithIdentity(trigger1, group1) .WithRecurrenceSchedule(FREQMONTHLY;BYDAY2MO, b b .InTimeZone(TimeZones.FindById(Eastern Standard Time))) .StartNow() .Build();从实现层面看RecurrenceTriggerImpl.TimeZone 在未显式设置时惰性初始化为TimeZoneInfo.LocalRRULE 引擎 GetNextOccurrence 先将dtStart、after、endTime转换到目标时区的本地墙钟时间再展开候选点最后再转回DateTimeOffset。这意味着同一 RRULE 在不同时区下产生的 UTC 触发时刻不同部署跨地域集群时务必显式统一时区否则调度时刻会随节点本地时区漂移。持久化时时区 ID 会一并存入 JobStore见下文重启后仍能还原。DI / Hosted Service 集成AddQuartz 中的配置方式在使用services.AddQuartz(...)的宿主场景下同样通过WithRecurrenceSchedule配置触发器services.AddQuartz(q { q.AddJobMyJob(j j.WithIdentity(myJob)); q.AddTrigger(t t .ForJob(myJob) .WithIdentity(myTrigger) .WithRecurrenceSchedule(FREQMONTHLY;BYDAY2MO) .StartNow()); });WithRecurrenceSchedule是定义在 TriggerConfiguratorExtensions.cs 上的扩展方法提供两个重载接受裸 RRULE 字符串可带ActionRecurrenceScheduleBuilder配置委托或直接接受一个RecurrenceScheduleBuilder实例。因此无论是TriggerBuilder.Create()的独立构建还是 DI 容器内的触发器配置器调用方式完全一致。Misfire 指令与 CronTrigger 同语义的策略集RecurrenceTrigger 提供两个触发器专属 misfire 指令语义与 CronTrigger 对应指令相同另加通用IgnoreMisfirePolicyMisfireInstruction.RecurrenceTrigger.FireOnceNow立即补一次然后恢复原调度MisfireInstruction.RecurrenceTrigger.DoNothing跳过错过的触发从下一个计划时间继续MisfireInstruction.IgnoreMisfirePolicy忽略所有漏触发尽快补齐每次错过的触发可能连续密集补发多次。SmartPolicy默认等价于FireOnceNow。ITrigger trigger TriggerBuilder.Create() .WithIdentity(trigger1, group1) .WithRecurrenceSchedule(FREQWEEKLY;BYDAYMO, b b .WithMisfireInstruction(RecurrenceTriggerMisfireInstruction.DoNothing)) .Build();从源码角度RecurrenceTriggerMisfireInstruction枚举RecurrenceTriggerMisfireInstruction.cs的四个成员SmartPolicy、IgnoreMisfires、FireAndProceed、DoNothing直接映射到存储层的 int 编码MisfireInstruction.SmartPolicy0、IgnoreMisfirePolicy-1、FireOnceNow与DoNothing为 Recurrence 家族专属编码与各触发器族共用同一MisfireInstructionCode存储位因此可安全互转。UpdateAfterMisfire的实现RecurrenceTriggerImpl.cs印证了策略语义DoNothing会依据日历Calendar向前搜索下一个可触发时刻FireOnceNow直接把NextFireTimeUtc置为当前时间IgnoreMisfirePolicy直接返回不做任何调整。ValidateMisfireInstruction只接受[-1, DoNothing]闭区间内的编码超出范围的指令会校验失败。何时用 RecurrenceTrigger何时用其他触发器场景推荐触发器固定间隔每 10 秒SimpleTriggerCron 可表达的周期每个工作日上午 9 点CronTrigger月内第 N 个星期几第二个周一RecurrenceTrigger月末最后一个工作日RecurrenceTrigger每隔一周的特定几天RecurrenceTrigger带 BYSETPOS 的复杂年度模式RecurrenceTrigger日历间隔每 5 个月CalendarIntervalTrigger简单判断标准当需求能用 FREQ INTERVAL 之外的序数、星期几、集合取位等语义描述时优先考虑 RecurrenceTrigger能用固定间隔或标准 Cron 表达的继续使用 SimpleTrigger/CronTrigger 以保持配置简洁与生态兼容如 XML 调度数据。持久化无需改表RRULE 落库 QRTZ_SIMPROP_TRIGGERSRecurrenceTrigger 复用现有的QRTZ_SIMPROP_TRIGGERS表无需任何 schema 变更。持久化委托实现见 RecurrenceTriggerPersistenceDelegate.cs其落库映射为STR_PROP_1列存放 RRULE 字符串该列最大 512 字符——写入时若超长会抛出JobPersistenceExceptionINT_PROP_1列Int1存放TimesTriggered用于重启后还原 COUNT 进度时区 ID随SimplePropertiesTriggerProperties.TimeZoneId一并存储还原时通过TimeZones.FindById解析触发器类型判别符discriminatorRECUR由GetHandledTriggerTypeDiscriminator返回。读取端GetTriggerPropertyBundle会用持久化的 RRULE、时区与触发次数重建RecurrenceScheduleBuilder并回填TimesTriggered保证集群重启或故障转移后调度状态无缝延续。由于时区 ID 与 RRULE 都依赖 JobStore 的持久化列使用 ADO.NET JobStore 时请务必遵守 512 字符上限超长规则建议改用RecurrenceTriggerImpl直构并评估改用其他存储策略。源码深挖惰性计算与快速前进优化RRULE 引擎的核心设计目标是不物化所有发生时刻。RecurrenceRule.GetNextOccurrence从dtStart出发按周期逐个展开候选点并应用 BY* 过滤直到找到严格晚于after的第一个有效时刻子日频率SECONDLY/MINUTELY/HOURLY配合限制性 BY* 规则时例如FREQSECONDLY;BYHOUR23从午夜起步会启用TryFastForwardSubDaily快速跳过不可能命中的周期避免在 10 万次迭代上限MaxIterations 100_000内穷举见 RecurrenceRule.cs。FinalFireTimeUtc的处理也值得注意带 COUNT 的规则调用GetNthOccurrence直接定位第 N 个发生点扫描上限MaxNthIterations 10_000_000个周期带 UNTIL 或 EndTime 的规则则调用GetLastOccurrenceBefore从边界倒着向前搜索且子日频率同样有TryFastRewindSubDaily快速回退。两个方向上的快速前进/回退保证了稀疏但长周期的规则如FREQMINUTELY;BYMONTH12不会因逐周期扫描而性能退化。对绝大多数调度场景GetFireTimeAfter的单步惰性求值即已足够你无需关心这些内部优化但了解其存在有助于评估上百万次触发量的规则是否安全这类问题。小结RecurrenceTrigger 把标准 RRULE 语言带入了 Quartz.NET 调度体系一条字符串即可表达 Cron/Simple 无法覆盖的序数与集合语义惰性求值保证长周期调度的高效性misfire 策略与既有触发器族保持一致持久化复用QRTZ_SIMPROP_TRIGGERS免去数据库迁移。实践要点可归纳为四条FREQ 必填且 COUNT/UNTIL 互斥、COUNT 按实际触发次数计数、跨时区部署务必显式InTimeZone、RRULE 落库受 512 字符上限约束。在选型时凡是第几个星期几、每两周的几天、每月最后一天、按位置取集合这类需求RecurrenceTrigger 都是比 Cron 更自然、更可读的答案。想要更深入理解实现建议继续阅读 RecurrenceRule.cs解析与求值、RecurrenceTriggerImpl.cs触发生命周期、RecurrenceTriggerPersistenceDelegate.cs持久化以及 RecurrenceTriggerSamples.cs全部可运行示例。赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐Quartz.NET CronTrigger 完全指南Cron 表达式语法、调度构建与 Misfire 策略Quartz.NET CronTrigger 完全指南Cron 表达式语法、调度构建与 Misfire 策略 CronTrigger 是 Quartz.NET任务调度后端Quartz.NET CronTrigger 完全指南Cron 表达式语法、构建方式与 Misfire 策略Quartz.NET CronTrigger 完全指南Cron 表达式语法、构建方式与 Misfire 策略 CronTrigger 是 Quartz.NET任务调度后端Quartz.NET CronTrigger 完整指南cron 表达式语法、H 哈希分散调度与 CronExpressionBuilder 编程式构建Quartz.NET CronTrigger 完整指南cron 表达式语法、H 哈希分散调度与 CronExpressionBuilder 编程式构建 Cro任务调度后端上一篇G-Helper启动失败终极解决方案从识别到系统化修复的完整指南下一篇华硕笔记本控制神器G-Helper3分钟快速上手指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考