最常见原因是调度器未显式启动或启动时机错误:ischeduler默认处于standby状态,必须在ihostedservice.startasync()或applicationstarted事件中调用await scheduler.start()才能激活;漏掉await、写成同步调用或在configureservices中启动均会导致静默失败。

Quartz.NET 不是“装完就能跑”的定时器,它必须显式启动、严格遵循生命周期规则,否则注册了 job 和 trigger 也完全不触发——这是绝大多数人踩坑的第一步。
为什么 Quartz.NET 任务注册了却从不执行
最常见原因是调度器没真正启动,或者启动时机错了。IScheduler 默认处于 STANDBY 状态,await scheduler.Start() 是唯一让它活过来的操作,漏掉或忽略 await 都会导致静默失败。
- ASP.NET Core 中,
await scheduler.Start()必须放在IHostedService.StartAsync()或ApplicationStarted事件里,不能写在ConfigureServices阶段——那时 DI 容器还没就绪,调度器拿不到依赖 - Console 应用里,
await scheduler.Start()后必须让主线程持续运行(比如Console.ReadLine()),否则进程立刻退出,调度器被释放 - 写成
schedule.Start();(没 await)是典型错误:方法返回 Task 但没等完成,后续ScheduleJob()可能因调度器未就绪而注册失败或延迟生效
Job 类写错导致无报错、无执行
Quartz 用反射创建 job 实例,对类定义极其敏感。出错时控制台往往安静如鸡,连日志都不打,排查起来特别隐蔽。
- 类必须是
public,不能是internal、static或嵌套类(nested class) - 必须有
public MyJob()无参构造函数;哪怕你只想注入ILogger,也不能写public MyJob(ILogger<myjob> logger)</myjob>,否则抛JobInstantiationFailedException,提示 “No suitable constructor found” -
Execute方法签名必须是Task Execute(IJobExecutionContext context);如果里面要调异步操作,得包一层await Task.Run(() => { ... }),直接await会阻塞线程池
Cron 表达式在秒级场景下不准的真相
“0/10 * * * * ?” 看似每 10 秒触发一次,实际很容易漏跑或堆积,尤其当 job 执行耗时超过间隔时间。
- 默认
misfire策略是SmartPolicy,对秒级 cron 不友好;应显式指定.WithMisfireHandlingInstructionFireAndProceed() - CronTrigger 本质是按“下次触发时间”计算,不是“固定周期轮询”。job 耗时 > 间隔时,后续触发可能被跳过,而不是补跑
- 高频任务(≤30 秒)优先用
SimpleTrigger:.WithSimpleSchedule(x => x.WithIntervalInSeconds(10).RepeatForever()),更稳、更可预测 - 确认
quartz.threadPool.threadCount至少为 3–5;内存模式默认 10 线程,但并发 job 多或单个 job 波动大时仍可能阻塞
时区错乱让 Cron 触发时间偏移 8 小时
本地开发时看着正常,部署到 Linux Docker 或云服务器后,“每天凌晨 2 点”变成上午 10 点——大概率是 CronTrigger 按 UTC 解析,而非你的本地时区。
- 必须显式指定时区:
CronScheduleBuilder.CronSchedule("0 0 2 * * ?").InTimeZone(TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")) - Linux 容器需确保时区数据已加载(如 Alpine 镜像要装
tzdata包),否则FindSystemTimeZoneById可能返回 null - 避免在
ConfigureServices里直接 newStdSchedulerFactory;应通过ISchedulerFactory.GetScheduler()获取,否则生命周期管理失效,应用重启后 job 丢失
真正难的从来不是写几行配置,而是理解 Quartz 的“状态机”本质:STANDBY → STARTED → SHUTDOWN,以及它对反射、线程、时区、misfire 的强约束。任何一步绕开这些机制,都会让任务在某个环境里突然失联,而且很难复现。











