☰
Quartz.NET 4.x 数据库 Schema 完全指南:表结构、迁移升级与索引调优
2026/10/7 1:51:01 网站建设 项目流程
  • 任务调度
  • 后端

【免费下载链接】quartznet

Quartz Enterprise Scheduler .NET

项目地址:https://gitcode.com/gh_mirrors/qu/quartznet
点击查看免费下载

Quartz.NET 使用 ADO.NET 持久化 Job Store(通常是LocalTransactionJobStore)时,需要一组固定的数据库表来存放调度数据。这篇指南以 Quartz.NET 4.x 官方文档的 Database Schema 为核心,结合仓库中的建表脚本、迁移脚本与源码实现,为你完整拆解:每张表存什么、4.x 强制要求的列与表、QRTZ_TRIGGERS的列语义与状态机、获取索引的设计与性能基准,以及从 3.x 升级到 4.x 的完整操作步骤。读完你将能够独立完成 Quartz.NET 持久化存储的建表、校验、升级与索引调优。

创建 Schema 与迁移的本质区别

一个 ADO.NET Job Store 需要一组表。在 Quartz.NET 4.x 中,创建和迁移是两条完全不同的路径:

  • 创建可以自动:ProvisionSchema()让 store 在启动时为其数据库执行 DDL,创建缺失的对象。它是选择加入的(opt-in),因为建表需要生产数据库通常不授予的权限。
  • 迁移永远是手动步骤:Quartz 内部没有任何机制帮你迁移已有 schema。

为什么迁移必须手动?因为 Quartz 的表不携带版本标记。一个有保护的CREATE TABLE只会跳过已存在的表,而不会检查这个表"现在应该有哪些列";要加版本标记,这本身就是一次迁移。因此部署流水线运行的是 database/migrations,其文件夹名就是版本号。作为对比:Hangfire 会记录 schema 版本并在启动时默认迁移,将其最重的 1.8 迁移放在一个 opt-in 开关后面;TickerQ 则把两步都交给应用的 EF Core 迁移。

Schema 创建:自动但仅限创建

ProvisionSchema()(对应AdoJobStoreOptions.SchemaProvisioning)有三种取值:

值Store 初始化时的行为
None什么都不做;缺失的表会让第一条命名它的语句失败
Validate默认值;检查表以及 4.x 新增的列,缺失则拒绝启动
CreateIfMissing为配置的数据库执行 DDL,然后验证;ProvisionSchema()设置的就是它

值得注意的细节:

  • Validate会对 store 的表执行SELECT 1,并对 3.x 已有表上每个 4.x 新增的列执行SELECT <column> … WHERE 1 = 0,然后点名指出缺什么。
  • CreateIfMissing不会"补列":如果一张表存在但缺少所需列,它不会执行 DDL——这意味着该 schema 不是 4.x 的。
  • 自动创建的 DDL 是一组按方言内嵌在Quartz.dll的脚本(src/Quartz/Impl/AdoJobStore/Schema,每方言一个create_<dialect>.sql),表前缀是占位符而非写死的QRTZ_,且没有GO、/或SET TERM,因为每个语句都是单独发送的。构建和集成测试会校验它创建的表格、列和索引与 database/tables 完全一致。
  • 它只创建,永不升级:每个语句都有保护,不会 drop 或 alter 任何东西。集群节点同时启动时也安全——只有先到者能创建某个对象,创建失败的节点会转而验证,若通过则说明其他节点已建好。
  • 六个方言各有一个create_脚本,但 SQL Server 的内存优化版(MOT)与 2016 之前版本没有对应脚本,也不存在各自的 driver delegate;对它们只能手动执行 tables_sqlServerMOT.sql / tables_sqlServer_Below2016.sql,并把SchemaProvisioning保持在Validate。

每张表存放什么

表存放内容
qrtz_calendars非标准日历
qrtz_job_detailsIJobDetail数据
qrtz_locksQuartz 获取的锁
qrtz_scheduler_stateIScheduler数据
qrtz_triggersITrigger数据
qrtz_cron_triggerscron 触发器的 cron 表达式
qrtz_fired_triggers当前正在运行的触发器
qrtz_blob_triggers以二进制 BLOB 存储的触发器
qrtz_simple_triggers简单重复触发器
qrtz_simprop_triggers自定义触发器;ICalendarIntervalTrigger、IDailyTimeIntervalTrigger和IRecurrenceTrigger使用它
qrtz_paused_trigger_grpsIScheduler.PauseTriggerGroups数据
qrtz_paused_job_grpsIScheduler.PauseJobGroups数据——每个被暂停的 job 组一行,因此空组也会被报告为已暂停

各数据库的建表脚本位于 database/tables,覆盖 SQL Server(含 2016+、内存优化 MOT、2012/2014 三个变体)、PostgreSQL、MySQL/MariaDB、Oracle、SQLite 与 Firebird 共八个脚本。

另一个常见坑是ADO.NET driver 是应用侧的包引用:UsePostgres需要Npgsql,UseSqlServer需要Microsoft.Data.SqlClient。缺失该包的项目能编译通过,但 store 初始化时会以Could not load file or assembly失败。完整的方法-包对应表(共八个方法)在 job-stores 教程:UseSqlServer、UsePostgres、UseMySql、UseMySqlConnector、UseOracle、UseFirebird、UseSqlite、UseSystemDataSqlite,外加用于"其他一切"的UseGenericDatabase(它使用无法分页的StdAdoDelegate,只在触发器很多时才真正吃亏)。

4.x 强制要求的列与表

这四个列在 3.x 上是可选的——3.x 启动时会探测它们,缺失就禁用对应功能并警告。4.x 移除了探测,四个列全部必需:

列所在表3.x 中作为可选列加入的版本
MISFIRE_ORIG_FIRE_TIMEQRTZ_TRIGGERS3.17
EXECUTION_GROUPQRTZ_TRIGGERS、QRTZ_FIRED_TRIGGERS3.18
PREFERRED_NODEQRTZ_TRIGGERS3.19
PREFERRED_NODE_AUTOQRTZ_TRIGGERS3.19

此外 4.x 还需要 3.x 从未有过的东西:QRTZ_TRIGGERS上的列RETRY_POLICY和RETRY_ATTEMPT,以及表QRTZ_PAUSED_JOB_GRPS——后者让一个空的 job 组可以被暂停,并让 job 组列表能正确报告paused。

应用 database/migrations/4.0 即可补齐其中缺失的任何一项(含整表)。该目录下每个语句都有保护,所以在已经具备部分对象的数据库上运行也是安全的。完整的 3.x → 4.x 变更清单见 Database Schema Changes。

从 3.x 升级到 4.x 是强制性的,因为 4.x 不再探测 3.x 的可选列。

QRTZ_TRIGGERS 表:所有触发器类型的公共数据

QRTZ_TRIGGERS存放所有触发器类型共享的数据。类型特定的数据则根据TRIGGER_TYPE进入QRTZ_CRON_TRIGGERS、QRTZ_SIMPLE_TRIGGERS、QRTZ_SIMPROP_TRIGGERS或QRTZ_BLOB_TRIGGERS。

列存放内容
SCHED_NAME该行所属的Scheduler:InstanceName。每张表都有此列,所以一个数据库可以承载多个 scheduler
TRIGGER_NAME、TRIGGER_GROUPTriggerKey。与SCHED_NAME一起构成主键
JOB_NAME、JOB_GROUP该触发器触发的 job 的JobKey
DESCRIPTIONITrigger.Description
NEXT_FIRE_TIME、PREV_FIRE_TIME触发时间,UTC ticks;没有则为 null
PRIORITYITrigger.Priority,在同一时刻到期的触发器之间打破平局
TRIGGER_STATE存储状态——见下文状态一节
TRIGGER_TYPECRON、SIMPLE、CAL_INT、DAILY_I、RECUR或BLOB:指明其余数据在哪张兄弟表
START_TIME、END_TIME调度生效的时间区间,UTC ticks
CALENDAR_NAMEQRTZ_CALENDARS中从调度排除时间的条目(如有)
MISFIRE_INSTRmisfire 指令的数值
MISFIRE_ORIG_FIRE_TIMEmisfire 处理器把触发器移走的那个原始触发时间,让 job 能看出自己错过了什么
EXECUTION_GROUP触发器的执行组
PREFERRED_NODE、PREFERRED_NODE_AUTO节点亲和性:哪个节点应获取该触发器,以及是否由该触发器自己认领了这个"钉子"
RETRY_POLICY、RETRY_ATTEMPT重试策略的存储字符串形式,以及当前一次 occurrence 已执行的重试次数
JOB_DATA触发器自己的JobDataMap,序列化后存储

关于重试列的特殊语义:RETRY_POLICY对不重试的触发器为NULL(这是默认值);RETRY_ATTEMPT在 4.x 写入的行上为0,在升级带过来的行上为NULL——store 把两者都读作"到目前为止没有重试"。

触发器状态:存储词汇与对外词汇

TRIGGER_STATE存放的是存储词汇StoredTriggerState(定义在 StoredTriggerState.cs 的Quartz.Extensibility命名空间),而不是应用看到的枚举。

TRIGGER_STATE含义
WAITING就绪,到期即可被取走。普通的静止状态
ACQUIRED某个节点已取走该触发器,即将触发
EXECUTING其 job 正在运行
COMPLETE不会再触发
BLOCKED其 job 标注了[DisallowConcurrentExecution],且它的另一次触发正在运行
PAUSED已暂停,直到恢复
PAUSED_BLOCKED已暂停,且被同一 job 的一次运行中的触发阻塞
ERROR触发器无法触发,通常是因为其 job 类型无法构建。IScheduler.ResetTriggerFromErrorState可清除它
DELETED触发器被移除过程中的瞬态标记

对外,IScheduler.GetTriggerState返回的是 TriggerState.cs 中的TriggerState:Normal、Paused、Complete、Error、Blocked、Executing,以及触发器不存在时的None。映射规则:

  • WAITING和ACQUIRED读作Normal;PAUSED_BLOCKED读作Paused;DELETED读作None。
  • 触发器 job 正在运行时读作Executing,除非行上写着 deleted、error 或 paused——这些按原样报告。
  • TriggerStateResolver.Resolve就是这段映射,源码见 TriggerStateResolver.cs。它实现的优先级为None > Error > Paused > Awaiting > Executing > Blocked > Complete > Normal:paused、error 与 awaiting 之所以压过 executing,是因为它们是操作者必须处理的事实,且在前一次触发遗留的执行仍在进行时依然成立;StoredTriggerState.Awaiting则是 4.2 新增的存储状态,用于携带 Continuation 的触发器等待另一触发器触发结束。

索引设计:特别是获取索引(acquisition index)

QRTZ_TRIGGERS上随 schema 附带四个索引,各方言一致:

  • 三个键查找:(SCHED_NAME, JOB_NAME, JOB_GROUP)、(SCHED_NAME, TRIGGER_GROUP, TRIGGER_NAME)、(SCHED_NAME, CALENDAR_NAME);
  • IDX_QRTZ_T_NFT_ST——两条触发器扫描都用它:获取(acquisition),以及带有计数 peek 的 misfire 恢复。它是唯一一个形状随方言变化的索引:
-- SQL Server、PostgreSQL、MySQL、Oracle、SQLite (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME ASC, PRIORITY DESC, MISFIRE_INSTR) -- Firebird (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME)

4.0 丢弃了第五个索引

4.0 删除了IDX_QRTZ_T_NFT_ST_MISFIRE(对应 issue #3656):这是 SQL Server、MySQL、Oracle、Firebird 上曾经存在的第五个索引,覆盖(SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE)(PostgreSQL 和 SQLite 从来没有它)。它以前导列MISFIRE_INSTR开头,而两条 misfire 语句都用<> -1比较该列——任何 B-tree 都无法越过不等值谓词去 seek。在四个引擎上逐计划实测(issue #3608、#3656),没有任何优化器会为两条 misfire 语句选它;MySQL 看起来选了,只是因为MySQLDelegate用名字强制指定了它,而现在这个 hint 指向的是获取索引。从 3.x 升级时,删除动作在schema_30_to_40_indexes_<dialect>.sql中,独立成文件是因为 3.x确实从该索引扫描 misfire,所以要等最后一个 3.x 节点关闭后再执行。

获取索引最后两列服务于执行计划而非谓词

获取索引的PRIORITY DESC与MISFIRE_INSTR两列服务于查询计划,而不是谓词(issue #3510)。SelectNextTriggerToAcquire按NEXT_FIRE_TIME ASC, PRIORITY DESC排序,每个方言都把行数限制放进该语句,因此ORDER BY会在平局集合中选出优先级最高的触发器——与RAMJobStore行为一致。当索引方向与排序方向匹配时,引擎直接取第一条记录,而无需读入并排序所有候选:

100,000 个触发器,5,000 个到期,单次获取SQL Server 2022MySQL 8.0PostgreSQL 15Firebird 4
p50,三列版本21.6 ms11.8 ms0.89 ms14.8 ms
p50,发布形态0.59 ms0.69 ms0.74 ms14.8 ms
读取数,三列版本20,39515,5171,52610,025
读取数,发布形态8962410,025
索引体积+6.2 %+13 %+0.4 %不变

MISFIRE_INSTR作为第五列服务于 misfire 积压场景。有序 seek 从最旧的等待触发器开始,而语句对NEXT_FIRE_TIME的下界处在与MISFIRE_INSTR的OR中,无法收窄 seek。没有这一列,seek 走过的每一行积压记录都要付出一次表查找;有了它,这些行在索引内就被跳过:面对 5,000 行积压,SQL Server 的逻辑读从 20,401 降到84,MySQL 的缓冲读从 15,071 降到117。

唯一的回退场景:窗口之下有数千个 misfired 触发器、到期者只有寥寥几个时,约 3 ms 对 1.5 ms。而到期者有数千个时则是 3 ms 对 30 ms,且积压会快速排空。

对齐索引定义前的三个注意事项

  • Firebird 保留三列索引:它的索引整体只能升序或降序,CREATE INDEX会拒绝ASC,而带负优先级取反的计算列也无法建索引(报attempt to index COMPUTED BY column)。那里的获取仍然要对候选排序——即上表 10,025 次读取。单按NEXT_FIRE_TIME排序实测快 3.2 倍、便宜 345 倍,但那会改变平局集合中先触发哪个触发器;如果这让你付出代价,请带着数据开 issue,而不是自行修补 schema。
  • MySQL 8.0.1 之前与 MariaDB 10.8 之前会解析索引里的DESC然后忽略它。无害,只是没买到任何东西。
  • Oracle会把降序键列变成函数索引,因此USER_IND_COLUMNS会在该位置显示一个隐藏的SYS_NC000nn$列并带DESC。这是预期行为。

PREFERRED_NODE 不在任何索引中

PREFERRED_NODE不在任何索引里,这一点在 PostgreSQL 15、SQL Server 2022 和 MySQL 8.0 上以 100,000 个触发器实测过(issue #3426);AcquisitionIndexBenchmark 可按需打印执行计划。原因与结论:

  • 它对获取毫无帮助:节点亲和性过滤器是一个析取(未钉住、钉在此节点、或钉在已停止 check-in 的节点),而 B-tree 无法服务于OR,三个引擎上的执行计划都不变。把它加进获取索引,在 SQL Server 和 MySQL 上相比MISFIRE_INSTR一无所获,还会让 PostgreSQL 的积压场景更糟——660 个共享缓冲对 408 个。
  • 它只对故障转移的 re-pin 有帮助:即ClusterRecover对每个死节点发出的那一次UPDATE——从全表扫描(PostgreSQL 8.4 ms、SQL Server 4,319 逻辑读、MySQL 约 88 ms)变成两三页的 seek。这每个节点故障只跑一次,不值得在最繁忙的表上付出永久的写成本。
  • 如果仍要自己加,用(SCHED_NAME, PREFERRED_NODE, PREFERRED_NODE_AUTO),且只针对故障转移足够频繁、多秒级恢复真会痛的集群。先测量:触发器不到几千个时,无论索引如何,每个计划都只读寥寥几页。

从 3.x 升级到 4.x 的实操步骤

升级分两个文件、两个时机,参见 database/migrations/4.0 与 Database Schema Changes:

文件状态何时执行
schema_30_to_40_upgrade_<db>.sql强制现在。它并入 3.17、3.18、3.19,每个语句都有保护,全部在 3.x 节点仍在运行时也安全
schema_30_to_40_indexes_<db>.sql可选,纯性能最后一个 3.x 节点关闭之后,或离线升级时紧随第一个文件之后。它取代了 3.20

升级文件的五个章节(SQL Server 版见 schema_30_to_40_upgrade_sqlServer.sql):

  1. MISFIRE_ORIG_FIRE_TIME列(必需)——bigint NULL;
  2. EXECUTION_GROUP列(必需)——QRTZ_TRIGGERS与QRTZ_FIRED_TRIGGERS各一个nvarchar(200) NULL;
  3. PREFERRED_NODE/PREFERRED_NODE_AUTO(必需)——nvarchar(200) NULL与bit NOT NULL DEFAULT 0,两列必须一起加,Quartz 仅在两者都存在时启用节点亲和性;
  4. RETRY_POLICY/RETRY_ATTEMPT(必需且 4.x 新增)——nvarchar(250) NULL与int NULL,可空无默认,现有行直接读作"无重试策略",无需回填数据;
  5. QRTZ_PAUSED_JOB_GRPS表(必需且 4.x 新增)——主键(SCHED_NAME, JOB_GROUP),镜像QRTZ_PAUSED_TRIGGER_GRPS。3.x 暂停 job 组时不做任何记录,所以IsJobGroupPaused对所有组都返回false,重启后暂停丢失;4.x 记录组名,使JobGroup.Paused准确、QueryJobGroups(new JobGroupQuery { Paused = true })能列出它们,且没有 job 的组也可以被暂停。

索引文件的内容与理由:

  • 重塑IDX_QRTZ_T_NFT_ST为(SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME ASC, PRIORITY DESC, MISFIRE_INSTR)(Firebird 除外,issue #3510),并删除IDX_QRTZ_T_NFT_ST_MISFIRE(issue #3656)。先建后删、文件内从上到下执行,schema 永远不会处于两个索引都没有的状态;全部语句有保护,重复执行无副作用。
  • 4.x 启动校验会检查每张需要的表是否可查询,并对该迁移加到 3.x 已有表上的每个列执行一次SELECT <column> … WHERE 1 = 0;缺表或缺任何一列都会拒绝启动,报错信息会点名缺失的列与脚本。注意该校验看不到列的类型与宽度,所以要运行完整脚本。
  • 在只运行了升级文件、未运行索引文件的数据库上,4.x 节点也能启动——只是扫描代替 seek,在大 schema 上才显著。

另外两项相关的升级提示:

  • SQLite 触发器命名:当前 tables_sqlite.sql 把四个引用完整性触发器命名为QRTZ_DELETE_SIMPLE_TRIGGER等(带表前缀)。已有数据库无需任何操作(Quartz 从不按名字引用它们);只有当你希望两个 Quartz schema 共享一个 SQLite 文件时才需要重命名重建。
  • 执行历史表是可选的:database/migrations/4.2 中的add_execution_history_<db>.sql创建QRTZ_EXECUTION_HISTORY与QRTZ_MISFIRE_HISTORY两张表,只有调用UsePersistentStore(store => store.UseExecutionHistory())的 store 才读写它们。全新安装或ProvisionSchema()会顺带创建它们,因此只有 4.0/4.1 创建的数据库需要该文件。

速查:什么时候该做什么

场景操作
全新数据库,任何方言运行 database/tables 中对应脚本(注意 SQL Server 脚本需先改USE [enter_db_name_here];,且脚本默认会 drop 旧 schema,用DropDb变量关闭);或让 store 在开发/测试环境用ProvisionSchema()自建
已有 3.x 数据库,升级到 4.x先运行schema_30_to_40_upgrade_<db>.sql(强制),最后一个 3.x 节点下线后再运行schema_30_to_40_indexes_<db>.sql(可选)
已有 4.0/4.1 数据库,升级到 4.2运行add_continuations_ .sql(强制,可混合窗口执行,但先迁移再滚节点再开始排 continuation);要用集群执行历史再运行add_execution_history_<db>.sql
触发器量大、获取慢确认IDX_QRTZ_T_NFT_ST已是 4.x 的五列形态;Firebird 例外
集群频繁故障转移、恢复慢测量后自行添加(SCHED_NAME, PREFERRED_NODE, PREFERRED_NODE_AUTO)索引

迁移脚本的完整历史(2.0 → 4.2)见 Database Schema Changes,同一内容的仓库内版本见 database/README.md。务必在测试环境、对生产数据库的副本先行演练——这是 Quartz.NET 官方对每一次 schema 迁移的明确要求。

  • 任务调度
  • 后端

【免费下载链接】quartznet

Quartz Enterprise Scheduler .NET

项目地址:https://gitcode.com/gh_mirrors/qu/quartznet
点击查看免费下载

相关推荐

上一篇:Yaf框架安全指南:防止常见Web攻击的10个防护策略
下一篇:5分钟快速上手:HubProxy 自建加速服务从零到部署

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询