quartz.net不可用timer替代,因其支持cron表达式调度、持久化、集群容错等生产级能力;需调用start()启动调度器,生产环境须用adojobstore;crontrigger应显式设utc时区;参数通过jobdatamap传递;任务依赖需手动触发或监听器实现;集群重复执行需正确配置并保障幂等性。

Quartz.NET 为什么不能直接用 Timer 替代?
因为 Timer 只能做简单间隔触发,不支持表达式调度(如“每周一凌晨2点”)、任务持久化、集群容错、暂停/恢复/中断等生产级能力。Quartz.NET 的核心价值在于它把调度逻辑和业务逻辑解耦,并通过 IScheduler 统一管理生命周期。
常见踩坑点:StdSchedulerFactory.GetDefaultScheduler() 返回的是未启动的调度器,必须显式调用 Start() 才会真正开始触发任务;另外,若使用默认内存存储(RAMJobStore),应用重启后所有任务丢失——生产环境务必配 AdoJobStore 并连数据库。
如何定义一个带参数的复杂触发器?
复杂定时往往不是固定间隔,而是依赖 Cron 表达式或多个触发条件组合。Quartz.NET 中,CronTrigger 是最常用的选择,但要注意:它的时区默认是系统本地时区,跨服务器部署时容易出错,建议显式指定 UTC:
var trigger = TriggerBuilder.Create()
.WithIdentity("daily-report-trigger")
.WithCronSchedule("0 0 2 * * ?") // 每天凌晨2点(秒 分 时 日 月 周)
.InTimeZone(TimeZoneInfo.Utc)
.Build();如果需要动态传参给任务(比如不同租户不同执行逻辑),不要在 IJob 构造函数里硬编码,而是用 JobDataMap:
job.SetJobData(new JobDataMap { ["tenantId"] = "t-123", ["env"] = "prod" });- 在
Execute(IJobExecutionContext context)中取:context.JobDetail.JobDataMap.GetString("tenantId")
注意:JobDataMap 只支持可序列化类型,DateTimeOffset 可以,但 SqlConnection 或委托不行。
如何让多个任务按依赖顺序执行?
Quartz.NET 本身不提供原生的“任务A完成后才触发任务B”机制,但可通过以下方式模拟:
- 在任务A的
Execute()结尾手动触发任务B:await scheduler.TriggerJob(new JobKey("job-b")); - 用
JobListener监听任务A的成功完成事件,再触发任务B(更解耦) - 将A+B合并为一个任务,内部用状态机控制流程(适合强耦合场景)
不推荐用 Thread.Sleep() 等待或轮询,这会阻塞线程池;也不建议在监听器中做耗时操作,否则拖慢整个调度线程。若依赖链较长(A→B→C→D),应考虑改用工作流引擎(如 ESB 或 Dapr)而非硬塞进 Quartz。
集群模式下为什么任务重复执行?
典型现象:同一任务在多个节点上同时跑,日志里看到两条几乎完全一致的执行记录。根本原因通常是没正确配置集群相关属性,或数据库表未初始化。
必须检查三点:
-
quartz.jobStore.type设为Quartz.Impl.AdoJobStore.JobStoreTX, Quartz,且quartz.jobStore.clustered设为true - 数据库已运行官方提供的建表 SQL(如
tables_sqlserver.sql),特别注意QRTZ_LOCKS表必须存在 - 所有节点的
quartz.scheduler.instanceId不能全设为AUTO—— 若网络不稳定,可能生成重复 ID;建议用唯一标识如HOSTNAME+PID
集群下,Quartz 会自动选举一个节点作为“主调度器”,其他节点只做备份。但如果数据库连接超时或锁表失败,仍可能短暂出现双触发——这是分布式系统的固有边界,业务代码需具备幂等性。
真正难处理的,是那些无法轻易幂等化的任务,比如发短信、扣库存。这时候得靠外部协调服务或数据库行锁兜底,Quartz 自身不解决这个问题。










