Appearance
Quartz.NET 任务调度入门与持久化实践
在业务系统中经常需要处理后台定时任务(如定时发送邮件、数据跑批等)。Quartz.NET 是非常流行的 Java 调度系统 Quartz 的 C# 移植版。它功能齐全,不仅支持简单的间隔触发,还支持复杂的 Cron 表达式、日历排除、数据持久化和集群负载均衡。
一、 核心概念:调度三要素
使用 Quartz.NET 执行定时任务,通常围绕三个核心对象展开:
- Job(作业):定义任务“做什么”。
- Trigger(触发器):定义任务“什么时候做”。
- Scheduler(调度器):将 Job 和 Trigger 绑定,负责在正确的时机执行任务。
1. 创建 Job (定义业务逻辑)
Job 必须实现 IJob 接口。参数传递通过 JobDataMap 进行。
csharp
public class SayHelloJob : IJob
{
// 可以通过属性自动绑定传入的 JobDataMap 参数
public string UserName { get; set; }
public Task Execute(IJobExecutionContext context)
{
return Task.Factory.StartNew(() =>
{
Console.WriteLine($"Hello {UserName}!");
});
}
}JobData 持久化特性: 如果你希望任务在每次执行后保存修改过的数据(如累加执行次数),可以在 Job 类上标记 [PersistJobDataAfterExecution] 特性,并在 Execute 中更新 context.JobDetail.JobDataMap。
2. 创建 Trigger (定义执行计划)
触发器主要有两种:
SimpleTrigger:适合简单的重复任务。
csharpvar trigger = TriggerBuilder.Create() .WithSimpleSchedule(m => m.WithRepeatCount(3).WithIntervalInSeconds(1)) .Build();CronTrigger:适合复杂的业务周期(如朝九晚五)。
csharpvar trigger = TriggerBuilder.Create() .WithCronSchedule("*/1 * * * * ?") .Build();
补充:日历排期 (Calendar) 触发器还能绑定日历,用于排除特定日期(如法定节假日不执行):
csharpvar calendar = new HolidayCalendar(); calendar.AddExcludedDate(DateTime.Today); await scheduler.AddCalendar("holidayCalendar", calendar, false, false); // 绑定日历 var trigger = TriggerBuilder.Create() .ModifiedByCalendar("holidayCalendar") .Build();
3. 注册并启动 Scheduler
调度器通常在应用程序启动时作为单例存在。
csharp
static async Task MainAsync()
{
// 1. 创建调度器
var schedulerFactory = new StdSchedulerFactory();
var scheduler = await schedulerFactory.GetScheduler();
await scheduler.Start();
// 2. 封装 JobDetail
var jobDetail = JobBuilder.Create<SayHelloJob>()
.SetJobData(new JobDataMap() {
new KeyValuePair<string, object>("UserName", "Tom")
})
.WithIdentity("SayHelloJob-Tom", "DemoGroup")
.Build();
// 3. 绑定并调度
await scheduler.ScheduleJob(jobDetail, trigger);
}二、 监听器机制 (Listeners)
Quartz.NET 提供了强大的监听机制,允许在运行时拦截和监控调度状态:
- JobListener:监听 Job 执行前 (
JobToBeExecuted)、执行后 (JobWasExecuted) 以及被否决 (JobExecutionVetoed) 的事件。 - TriggerListener:监听触发器的触发与错过,甚至可以在
VetoJobExecution中返回true动态否决一次 Job 的执行。 - SchedulerListener:监听全局调度器事件(添加/删除 Job、暂停触发器、内部严重错误等)。
注册示例:
csharp
// 注册针对所有 Group 的 Job 监听器
scheduler.ListenerManager.AddJobListener(new MyJobListener(), GroupMatcher<JobKey>.AnyGroup());三、 高级场景:持久化与负载均衡
默认情况下,Quartz.NET 将任务数据存储在内存中(RAMJobStore)。这速度极快,但程序重启会丢失所有调度状态,且无法横向扩展。企业级应用通常需要持久化到数据库。
1. 配置 ADO.NET 持久化 (JobStore)
Quartz 支持几乎所有主流关系型数据库(SQL Server, MySQL, PostgreSQL, Oracle 等)。
需要在配置文件中(如 quartz.config)指定 JobStore 类型与连接:
ini
# 指定使用 AdoJobStore
quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz
quartz.jobStore.dataSource = myDS
quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.PostgreSQLDelegate, Quartz
# 配置数据库连接
quartz.dataSource.myDS.connectionString = Server=localhost;Database=quartz_store;userid=admin;password=123;
quartz.dataSource.myDS.provider = Npgsql注意:使用持久化前,必须在数据库中手动执行 Quartz 官方提供的建表 SQL 脚本。
2. 集群与负载均衡 (Clustering)
当业务量大或需要高可用时,可以在多台服务器部署同一套代码连接同一个持久化数据库。通过以下配置开启集群:
ini
quartz.jobStore.clustered = true
# 自动生成当前实例 ID 避免冲突
quartz.scheduler.instanceId = AUTO开启后,数据库将作为中心锁,自动分配任务,避免同一个 Trigger 被两台机器同时执行。
⚠️ 踩坑点:集群模式下,所有服务器的时钟必须高度同步(建议开启 NTP 自动对时),否则可能导致调度错乱或锁竞争异常。
四、 常见问题与集成
- 依赖注入 (DI) 原生 Quartz.NET 在实例化 Job 时不支持复杂的构造函数依赖注入。在现代 ASP.NET Core 或 Topshelf 宿主中,通常需要借助
Autofac等第三方容器库。Install-Package Topshelf.AutofacInstall-Package Autofac.Extras.Quartz
- 并发控制 如果任务耗时过长,可能会发生上一次还没执行完,下一次触发时间又到了。可以在 Job 类上标记
[DisallowConcurrentExecution]特性,强制串行执行。
