quartz.net任务未触发的首要原因是未调用ischeduler.start(),导致触发器处于standby状态;job类须有无参构造函数,依赖服务应在execute中通过createscope获取;cron表达式必须为6位;并发执行需用[disallowconcurrentexecution]避免死锁。

Quartz.NET 任务没触发?先检查 IScheduler.Start() 调用时机
很多任务“写好了但根本不跑”,根本原因不是配置错,而是调度器压根没启动。Quartz.NET 不像 Windows 服务那样装完就自启——IScheduler 实例创建后必须显式调用 Start(),否则所有触发器都处于 STANDBY 状态,日志里也几乎不报错。
常见错误现象:StdSchedulerFactory.GetScheduler() 后直接定义 Job 和 Trigger,没等 await scheduler.Start() 就结束了程序(比如在 Console 应用的 Main 方法末尾退出)。
- Web 应用中,务必在
ApplicationStarted或IHostedService.StartAsync()中调用Start(),不能放在构造函数或ConfigureServices - Console 应用需保持主线程存活(如
Console.ReadLine()),否则进程退出,调度器随之销毁 -
Start()是异步方法,若忽略 await,可能在启动完成前就执行了后续逻辑
Job 类必须有无参构造函数,且不能依赖注入 IServiceScope
Quartz.NET 默认使用 SimpleJobFactory 创建 Job 实例,它只认无参构造函数。如果你写了带参数的构造函数又没保留无参版本,运行时会抛 System.MissingMethodException: No parameterless constructor defined。
更隐蔽的问题是:有人试图在 Job 构造函数里直接注入 IDbContextFactory<appdbcontext></appdbcontext> 这类服务,结果发现 DbContext 操作失败或连接被释放——因为 Job 实例生命周期由 Quartz 管理,不在 ASP.NET Core 的作用域内。
- Job 类应保持无状态、无依赖,所有外部服务通过
JobDataMap或工厂模式延迟获取 - 需要 DI 支持?换用
Microsoft.Extensions.DependencyInjection.Quartz.ServiceCollectionExtensions提供的AddQuartz()+AddQuartzServer(),并配合JobFactory实现类接管实例创建 - 若必须用 DbContext,应在
Execute(IJobExecutionContext context)内部用serviceProvider.CreateScope()新建作用域,用完立即释放
CronTrigger 表达式里秒数字段不能省,.NET 版本要求严格
Java 版 Quartz Cron 是 6 位(秒 分 时 日 月 周),而 .NET 社区早期文档常误传为 5 位(分 时 日 月 周)。Quartz.NET 从 3.x 开始强制要求 6 位,写成 "0 0 * * * ?" 是合法的,但 "0 * * * ?" 会直接抛 Quartz.SchedulerException,提示表达式解析失败。
容易踩的坑还包括:
- 周几字段用数字(1=周日,7=周六)还是英文缩写(SUN、MON)?两者都支持,但混用易出错;推荐统一用数字+问号,如
"0 0 12 ? * 2-6"(工作日中午执行) - 年份字段(第 7 位)极少用,加了反而增加兼容性风险,官方示例默认不写
- 测试表达式?别靠猜——用
CronExpression.IsValidExpression("...")提前校验,比上线后看日志快得多
并发执行导致数据库死锁?给 Job 加 [DisallowConcurrentExecution]
默认情况下,同一个 Job 类的多个实例可以同时执行(比如上一次执行还没结束,下一次触发又到了)。这在发邮件、调第三方 API 时可能问题不大,但一旦涉及更新同一张表的某几行(比如定时刷新缓存计数器),就极易触发 SQL Server 死锁或 PostgreSQL 的 deadlock detected 错误。
[DisallowConcurrentExecution] 是最轻量的解决方式,它让 Quartz 自动对 Job 类名加分布式锁,确保同一时间最多一个实例运行。
- 该特性只对同一
IJob类型生效,不同 Job 类之间不受影响 - 注意:锁是基于 JobKey(名称+组名)的,如果动态生成 JobKey 且未去重,锁就失效
- 如果真需要并行但规避冲突,得改业务逻辑——比如把大任务拆成按 ID 区间分片,每个分片用独立 JobKey
Quartz.NET 的麻烦不在配置多,而在它的生命周期和执行模型跟 ASP.NET Core 的 DI/Scope 完全不重叠。最容易被忽略的,其实是 Job 实例的创建时机和作用域边界——那里没有 using 块帮你兜底,也没有 GC 自动清理未释放的数据库连接。











