Quartz.NET 4.x 数据库 Schema 完全指南:表结构、迁移升级与索引调优
任务调度后端【免费下载链接】quartznetQuartz 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需要NpgsqlUseSqlServer需要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.17EXECUTION_GROUPQRTZ_TRIGGERS、QRTZ_FIRED_TRIGGERS3.18PREFERRED_NODEQRTZ_TRIGGERS3.19PREFERRED_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。每张表都有此列所以一个数据库可以承载多个 schedulerTRIGGER_NAME、TRIGGER_GROUPTriggerKey。与SCHED_NAME一起构成主键JOB_NAME、JOB_GROUP该触发器触发的 job 的JobKeyDESCRIPTIONITrigger.DescriptionNEXT_FIRE_TIME、PREV_FIRE_TIME触发时间UTC ticks没有则为 nullPRIORITYITrigger.Priority在同一时刻到期的触发器之间打破平局TRIGGER_STATE存储状态——见下文状态一节TRIGGER_TYPECRON、SIMPLE、CAL_INT、DAILY_I、RECUR或BLOB指明其余数据在哪张兄弟表START_TIME、END_TIME调度生效的时间区间UTC ticksCALENDAR_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 中的TriggerStateNormal、Paused、Complete、Error、Blocked、Executing以及触发器不存在时的None。映射规则WAITING和ACQUIRED读作NormalPAUSED_BLOCKED读作PausedDELETED读作None。触发器 job 正在运行时读作Executing除非行上写着 deleted、error 或 paused——这些按原样报告。TriggerStateResolver.Resolve就是这段映射源码见 TriggerStateResolver.cs。它实现的优先级为None Error Paused Awaiting Executing Blocked Complete Normalpaused、error 与 awaiting 之所以压过 executing是因为它们是操作者必须处理的事实且在前一次触发遗留的执行仍在进行时依然成立StoredTriggerState.Awaiting则是 4.2 新增的存储状态用于携带 Continuation 的触发器等待另一触发器触发结束。索引设计特别是获取索引acquisition indexQRTZ_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 4p50三列版本21.6 ms11.8 ms0.89 ms14.8 msp50发布形态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 降到84MySQL 的缓冲读从 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 #3426AcquisitionIndexBenchmark 可按需打印执行计划。原因与结论它对获取毫无帮助节点亲和性过滤器是一个析取未钉住、钉在此节点、或钉在已停止 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.sqlMISFIRE_ORIG_FIRE_TIME列必需——bigint NULLEXECUTION_GROUP列必需——QRTZ_TRIGGERS与QRTZ_FIRED_TRIGGERS各一个nvarchar(200) NULLPREFERRED_NODE/PREFERRED_NODE_AUTO必需——nvarchar(200) NULL与bit NOT NULL DEFAULT 0两列必须一起加Quartz 仅在两者都存在时启用节点亲和性RETRY_POLICY/RETRY_ATTEMPT必需且 4.x 新增——nvarchar(250) NULL与int NULL可空无默认现有行直接读作无重试策略无需回填数据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_MISFIREissue #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 迁移的明确要求。赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐Quartz.NET 数据库 Schema 完全指南AdoJobStore 表结构、触发器状态与迁移路径Quartz.NET 数据库 Schema 完全指南AdoJobStore 表结构、触发器状态与迁移路径 Quartz.NET 的持久化作业存储ADO.NE任务调度后端Quartz.NET 数据库 Schema 迁移完整指南从 1.x 到 4.2 的升级路径与脚本详解Quartz.NET 数据库 Schema 迁移完整指南从 1.x 到 4.2 的升级路径与脚本详解 导读 Quartz.NET 从不自动迁移你的数据库 S任务调度后端RTranslator跨版本数据迁移数据库Schema升级完全指南RTranslator跨版本数据迁移数据库Schema升级完全指南 引言移动应用数据迁移的痛点与解决方案 你是否曾因应用升级导致用户数据丢失而收到差评是否人工智能AI 应用本地部署语音NLP移动开发上一篇Yaf框架安全指南防止常见Web攻击的10个防护策略下一篇5分钟快速上手HubProxy 自建加速服务从零到部署创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考