Skip to content

Quartz.NET 任务调度入门与持久化实践 ​

在业务系统中经常需要处理后台定时任务(如定时发送邮件、数据跑批等)。Quartz.NET 是非常流行的 Java 调度系统 Quartz 的 C# 移植版。它功能齐全,不仅支持简单的间隔触发,还支持复杂的 Cron 表达式、日历排除、数据持久化和集群负载均衡。

一、 核心概念:调度三要素 ​

使用 Quartz.NET 执行定时任务,通常围绕三个核心对象展开:

  1. Job(作业):定义任务“做什么”。
  2. Trigger(触发器):定义任务“什么时候做”。
  3. 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:适合简单的重复任务。

    csharp
    var trigger = TriggerBuilder.Create()
        .WithSimpleSchedule(m => m.WithRepeatCount(3).WithIntervalInSeconds(1))
        .Build();
  • CronTrigger:适合复杂的业务周期(如朝九晚五)。

    csharp
    var trigger = TriggerBuilder.Create()
        .WithCronSchedule("*/1 * * * * ?")
        .Build();

补充:日历排期 (Calendar) 触发器还能绑定日历,用于排除特定日期(如法定节假日不执行):

csharp
var 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 提供了强大的监听机制,允许在运行时拦截和监控调度状态:

  1. JobListener:监听 Job 执行前 (JobToBeExecuted)、执行后 (JobWasExecuted) 以及被否决 (JobExecutionVetoed) 的事件。
  2. TriggerListener:监听触发器的触发与错过,甚至可以在 VetoJobExecution 中返回 true 动态否决一次 Job 的执行。
  3. 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 自动对时),否则可能导致调度错乱或锁竞争异常。

四、 常见问题与集成 ​

  1. 依赖注入 (DI) 原生 Quartz.NET 在实例化 Job 时不支持复杂的构造函数依赖注入。在现代 ASP.NET Core 或 Topshelf 宿主中,通常需要借助 Autofac 等第三方容器库。
    • Install-Package Topshelf.Autofac
    • Install-Package Autofac.Extras.Quartz
  2. 并发控制 如果任务耗时过长,可能会发生上一次还没执行完,下一次触发时间又到了。可以在 Job 类上标记 [DisallowConcurrentExecution] 特性,强制串行执行。

方舟 · Ark