Quartz.NET 全面解析与实战指南

一、Quartz.NET 简介

Quartz.NET 是一个功能强大、开源的作业调度框架,源自 OpenSymphony 的 Quartz API 的 .NET 移植版本,用 C# 改写,适用于 Winform、ASP.NET 以及 ASP.NET Core 等各类 .NET 应用。它的核心能力可以概括为:在指定的时间或按照指定的周期性地执行任务------无论是每天早上 8 点发送一封汇总邮件、每隔 10 分钟轮询一次接口状态,还是在工作日的特定时段内执行数据同步。

Quartz.NET 的主要特性包括:

  • 支持 Cron 表达式,实现灵活的日历化调度
  • 持久化任务信息到数据库,避免应用重启后任务丢失
  • 分布式集群部署,支持负载均衡与故障转移
  • 动态管理任务,支持添加、暂停、恢复、删除作业
  • 深度集成 ASP.NET Core 依赖注入系统
  • 支持插件和监听器,可自定义调度逻辑和监控机制

版本说明Quartz.NET 3.0 是一个重大更新,引入了完整的 async/await 支持,并对 .NET Core 提供了原生支持,NuGet 包也进行了拆分,例如 Quartz.JobsQuartz.Plugins 等成为独立的依赖项。


二、核心概念与架构

核心组件

组件 描述
Job(作业) 实现 IJob 接口的类,包含具体要执行的业务逻辑
Trigger(触发器) 定义任务何时执行的规则,如时间间隔、Cron 表达式等
Scheduler(调度器) 核心引擎,负责管理和协调 Job 与 Trigger 的执行
JobStore(作业存储) 存储作业和触发器信息,支持内存和数据库两种模式
ThreadPool(线程池) 为任务执行提供线程资源

基本工作流程

  1. 定义 Job (实现 IJob 接口的类),编写业务逻辑
  2. 创建 Trigger,设定触发规则(时间间隔、Cron 表达式等)
  3. 通过 Scheduler 将 Job 和 Trigger 绑定并启动调度
  4. 调度器按照 Trigger 的规则,在适当的时机通过线程池执行 Job

3.x 的核心变化:全面异步化

从 3.0 版本开始,Quartz.NET 全面拥抱异步编程。IJobExecute 方法现在返回 Task,可以轻松包含 async 代码:

csharp 复制代码
// 3.x 的 Job 定义
public class MyJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        // 异步操作示例
        await Task.Delay(1);
    }
}

如果你没有任何异步逻辑,也可以直接返回 Task.CompletedTask


三、快速安装与配置

1. 安装 NuGet 包

bash 复制代码
# 核心包
dotnet add package Quartz

# ASP.NET Core 托管集成
dotnet add package Quartz.Extensions.Hosting

# 如需持久化支持(例如 SQL Server)
dotnet add package Quartz.Plugins
dotnet add package Quartz.Serialization.Json

2. 定义 Job 类

csharp 复制代码
using Quartz;

public class HelloJob : IJob
{
    private readonly ILogger<HelloJob> _logger;

    public HelloJob(ILogger<HelloJob> logger)
    {
        _logger = logger;
    }

    public Task Execute(IJobExecutionContext context)
    {
        _logger.LogInformation("Hello! 当前时间: {Time}", DateTime.Now);
        return Task.CompletedTask;
    }
}

Quartz.NET 通过 IJobExecutionContext 向作业传递上下文信息,其中包括 JobDataMap,可以在 Trigger 或 Scheduler 级别传递参数给 Job。

3. 注册 Quartz 到 DI 容器

Program.cs 中完成注册:

csharp 复制代码
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddQuartz(q =>
{
    // 注册 Job
    var jobKey = new JobKey("HelloJob");
    q.AddJob<HelloJob>(opts => opts.WithIdentity(jobKey));

    // 注册触发器 - SimpleSchedule 方式
    q.AddTrigger(opts => opts
        .ForJob(jobKey)
        .WithIdentity("HelloJob-trigger")
        .WithSimpleSchedule(x => x.WithIntervalInSeconds(10).RepeatForever()));
});

// 添加 Quartz 托管服务,随应用生命周期启停
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

var app = builder.Build();
app.Run();

四、触发器(Trigger)详解

Trigger 是 Quartz.NET 中最灵活的组件之一,定义了作业何时执行。

1. SimpleSchedule ------ 简单间隔调度

适用于"每隔 N 秒/分钟/小时执行一次"这类固定间隔的场景:

csharp 复制代码
q.AddTrigger(opts => opts
    .ForJob("HelloJob")
    .WithIdentity("simple-trigger")
    .WithSimpleSchedule(x => x
        .WithIntervalInMinutes(30)  // 每 30 分钟执行
        .RepeatForever()));         // 无限重复

还可以控制重复次数、开始时间和结束时间:x.WithRepeatCount(10) 表示总共执行 10 次后停止。

2. CronSchedule ------ Cron 表达式调度

当需要日历化的复杂调度规则时(如"每个工作日早上 9 点"),使用 Cron 表达式:

csharp 复制代码
q.AddTrigger(opts => opts
    .ForJob("HelloJob")
    .WithIdentity("cron-trigger")
    .WithCronSchedule("0 0 9 ? * MON-FRI")); // 工作日早上9点

3. Cron 表达式详解

Quartz.NET 使用 7 字段(年可选)的 Cron 表达式:

字段 是否必填 允许值 允许的特殊字符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * ? / L W
月份 1-12 或 JAN-DEC , - * /
星期 1-7 或 SUN-SAT , - * ? / L #
空或 1970-2099 , - * /

例如 0 0 12 ? * WED 表示"每周三中午 12:00 执行"。

常用特殊字符

字符 含义 示例
* 所有值 * * * * * ? 表示每秒
? 不指定值(日期和星期互斥) 日期字段为 ? 时不关心具体日期
- 范围 MON-FRI 表示周一至周五
, 列举 MON,WED,FRI 表示周一、三、五
/ 增量 0/15 在分钟字段表示每 15 分钟,从第 0 分钟开始
L 最后 L 在日期字段表示当月最后一天
# 第 N 个 6#3 在星期字段表示第 3 个周五

常用表达式示例

复制代码
0 0/5 * * * ?              # 每 5 分钟执行一次
0 0 2 * * ?                # 每天凌晨 2 点执行
0 15 10 ? * MON-FRI        # 周一至周五上午 10:15 执行
0 0 9 ? * 6L               # 每月最后一个周五上午 9 点
0 0/30 9-17 ? * MON-FRI    # 工作日 9:00-17:00 内每 30 分钟执行

4. 其他高级触发器

Quartz.NET 还支持 DailyTimeIntervalTrigger,可以方便地定义每日时间段内的调度:

csharp 复制代码
var trigger = TriggerBuilder.Create()
    .WithIdentity("officeHoursTrigger")
    .WithSchedule(DailyTimeIntervalScheduleBuilder.Create()
        .OnMondayThroughFriday()
        .StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(9, 0))
        .EndingDailyAt(TimeOfDay.HourAndMinuteOfDay(17, 0))
        .WithIntervalInHours(2))
    .Build();

五、依赖注入与 Job 工厂

Quartz.NET 天然支持 ASP.NET Core 的依赖注入系统。创建带依赖的 Job 非常简单:

csharp 复制代码
// 定义有依赖注入的 Job
public class EmailJob : IJob
{
    private readonly IEmailService _emailService;

    public EmailJob(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public async Task Execute(IJobExecutionContext context)
    {
        await _emailService.SendAsync("定时邮件", "来自 Quartz.NET 的问候");
    }
}

// 注册服务和 Job
builder.Services.AddTransient<IEmailService, EmailService>();

builder.Services.AddQuartz(q =>
{
    q.UseMicrosoftDependencyInjectionJobFactory(); // 启用 DI 工厂
    q.AddJob<EmailJob>(opts => opts.WithIdentity("EmailJob"));
});

UseMicrosoftDependencyInjectionJobFactory() 告诉 Quartz 使用 Microsoft 的 DI 容器来创建 Job 实例,这样构造函数中注入的服务就能被正确解析。


六、任务持久化(JobStore)

Quartz.NET 提供两种存储模式:

1. RAMJobStore ------ 内存存储

默认的存储模式,将所有数据保存在内存中,速度最快,但应用停止或崩溃后任务信息会丢失。适合测试环境或不在意任务丢失的轻量场景。

2. AdoJobStore ------ 数据库持久化

将 Jobs 和 Triggers 存储到关系型数据库中,确保任务在应用重启后不会丢失,也是集群部署的基础。

配置步骤

(1)首先创建数据库表------官方提供了 SQL 建表脚本,位于 database/dbtables 目录下,所有表默认以 QRTZ_ 为前缀。

(2)配置连接字符串和 JobStore:

Quartz.NET 4.1 及以上版本支持使用 appsettings.json 进行层次化 JSON 配置,更加直观:

json 复制代码
{
  "Quartz": {
    "Scheduler": {
      "InstanceName": "My Scheduler",
      "InstanceId": "AUTO"
    },
    "ThreadPool": {
      "MaxConcurrency": 10
    },
    "JobStore": {
      "Type": "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
      "DataSource": "default",
      "TablePrefix": "QRTZ_"
    },
    "DataSource": {
      "default": {
        "Provider": "SqlServer",
        "ConnectionString": "Server=localhost;Database=quartznet"
      }
    }
  }
}

(3)在代码中使用 JSON 配置:

csharp 复制代码
builder.Services.AddQuartz(Configuration.GetSection("Quartz"), q =>
{
    // 代码配置可与 JSON 配置并存
});

七、监听器(Listeners)------AOP 式任务监控

监听器是 Quartz.NET 中实现任务监控和横切关注点(AOP)的关键机制。通过实现监听器接口,可以在调度生命周期中插入自定义逻辑(如日志记录、性能监控、告警通知等)。

1. JobListener ------ 监听 Job 事件

接收与 Job 执行相关的三个事件:

csharp 复制代码
public class LoggingJobListener : JobListenerSupport
{
    public override string Name => "LoggingJobListener";

    public override ValueTask JobToBeExecuted(IJobExecutionContext context)
    {
        Console.WriteLine($"Job [{context.JobDetail.Key}] 即将执行...");
        return ValueTask.CompletedTask;
    }

    public override ValueTask JobWasExecuted(IJobExecutionContext context,
        JobExecutionException? jobException)
    {
        if (jobException == null)
            Console.WriteLine($"Job [{context.JobDetail.Key}] 执行成功");
        else
            Console.WriteLine($"Job [{context.JobDetail.Key}] 执行失败: {jobException.Message}");
        return ValueTask.CompletedTask;
    }
}

2. TriggerListener ------ 监听 Trigger 事件

接收与触发器相关的四个事件:

csharp 复制代码
public class MetricsTriggerListener : TriggerListenerSupport
{
    public override string Name => "MetricsTriggerListener";

    public override ValueTask TriggerFired(ITrigger trigger, IJobExecutionContext context)
    {
        // 触发器触发时记录指标
        return ValueTask.CompletedTask;
    }

    public override ValueTask TriggerMisfired(ITrigger trigger)
    {
        // 处理错过触发的情况
        Console.WriteLine($"Trigger [{trigger.Key}] 错过触发!");
        return ValueTask.CompletedTask;
    }
}

> 关键提示

  • 异常处理:确保监听器内不抛出未捕获异常,否则可能导致作业卡死
  • 不持久化:监听器注册在运行时,不会存入 JobStore,每次应用启动都需重新注册
  • 使用辅助基类 :继承 JobListenerSupportTriggerListenerSupport 可以只重写感兴趣的方法

3. 注册监听器

监听器通过 ListenerManager 注册,配合 Matcher 精确控制作用范围:

csharp 复制代码
scheduler.ListenerManager.AddJobListener(
    myJobListener,
    KeyMatcher<JobKey>.KeyEquals(new JobKey("myJobName", "myJobGroup")));

// 监听特定组的所有 Job
scheduler.ListenerManager.AddJobListener(
    myJobListener,
    GroupMatcher<JobKey>.GroupEquals("myJobGroup"));

// 监听所有 Job
scheduler.ListenerManager.AddJobListener(
    myJobListener,
    GroupMatcher<JobKey>.AnyGroup());

八、高级特性详解

1. 集群部署

Quartz.NET 支持多节点集群,实现负载均衡和故障转移。集群模式下,所有节点共享同一个数据库(通过 AdoJobStore),通过数据库锁机制确保同一个 Job 不会被多个节点同时执行。

配置要点

  • 所有共享同一套数据库和表结构
  • 设置 org.quartz.jobStore.isClustered = true
  • 各节点的线程池参数保持一致
  • 确保各节点时钟同步

适用场景:集群特性最适用于将长时间运行的计算密集型任务分布到多个节点执行,实现负载分担。

2. 多调度器(Multiple Schedulers)

Quartz.NET 4.x 开始,可以通过 AddQuartz(string name, ...) 创建命名调度器,实现工作负载隔离:

csharp 复制代码
// 快速的内存调度器(用于高频轻量任务)
builder.Services.AddQuartz("FastScheduler", q =>
{
    q.UseInMemoryStore();
    q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 5);
    q.ScheduleJob<NotificationJob>(trigger => trigger
        .WithSimpleSchedule(x => x.WithIntervalInSeconds(30).RepeatForever()));
});

// 持久化的数据库调度器(用于关键业务任务)
builder.Services.AddQuartz("DurableScheduler", q =>
{
    q.UsePersistentStore(s =>
    {
        s.UseSqlServer(sql => sql.ConnectionString = "your connection string");
    });
    q.ScheduleJob<ReportJob>(trigger => trigger
        .WithCronSchedule("0 0 2 * * ?"));
});

// 单次调用启动所有命名调度器
builder.Services.AddQuartzHostedService(options =>
{
    options.WaitForJobsToComplete = true;
});

这种做法可以让不同的调度器使用不同的存储策略、线程池和配置,彼此完全隔离。

3. 动态作业管理

通过 IScheduler 接口可以在运行时动态管理任务:

csharp 复制代码
// 动态添加作业
await scheduler.ScheduleJob(jobDetail, trigger);

// 暂停作业
await scheduler.PauseJob(jobKey);

// 恢复作业
await scheduler.ResumeJob(jobKey);

// 删除作业
await scheduler.DeleteJob(jobKey);

// 检查作业是否存在
bool exists = await scheduler.CheckExists(jobKey);

4. 传递参数 ------ JobDataMap

JobDataMapQuartz.NET 中向 Job 传递参数的机制,可以在 Trigger 定义时或 Scheduler 级别设置键值对:

csharp 复制代码
// 在 Trigger 定义时传递参数(示例)
q.AddTrigger(opts => opts
    .ForJob("HelloJob")
    .UsingJobData("RetryCount", "3")
    .UsingJobData("TimeoutSeconds", "30")
    .WithCronSchedule("0 0/10 * * * ?"));

生产建议:CronTrigger 应显式设置时区,参数通过 JobDataMap 传递,任务依赖需手动触发或使用监听器实现。


九、Quartz.NET vs Hangfire:如何选型?

两个框架都能实现定时任务,但定位和适用场景有明显差异:

维度 Quartz.NET Hangfire
核心定位 企业级调度引擎 通用后台任务处理框架
调度能力 复杂 Cron、日历调度、时间段约束 基本 Cron 表达式
持久化 可选(RAM / 数据库) 默认依赖数据库持久化
可视化面板 无(需第三方如 Quartzmin) 内置 Dashboard
集群支持 原生数据库锁机制 基于持久化存储的任务分发
学习曲线 较陡峭 较平缓
执行精度 毫秒级 秒级
最佳场景 复杂调度策略、金融系统、企业批处理 可靠任务执行、可观测性要求高的场景

选型建议

  • Hangfire 更适合快速集成、注重任务可观测性、需要内置 Dashboard 的场景------集成简单,自带监控面板
  • Quartz.NET 更适合需要精确、复杂调度策略的企业级系统------支持丰富 Cron 表达式、日历间隔等高级调度规则,在复杂调度场景下更为灵活

十、最佳实践与注意事项

1. Job 设计原则

  • 幂等性:Job 逻辑应设计为可安全重复执行的,避免集群或重试场景下产生重复数据。集群模式下,重复执行需正确配置并保障幂等性
  • 轻量化 :Job 的 Execute 方法应尽快完成,耗时长的工作应拆分到子任务或消息队列中异步处理
  • 异常处理 :在 Execute 方法中妥善处理异常,返回适当的 JobExecutionException 以控制重试行为
  • 避免线程阻塞:充分利用 async/await 避免阻塞调度线程

2. 配置建议

  • 生产环境必须使用 AdoJobStore,避免应用重启导致任务丢失
  • 合理设置线程池大小MaxConcurrency 应根据任务特点和服务器资源调整,默认为 10
  • 配置 WaitForJobsToComplete:优雅关闭时等待正在执行的任务完成

3. 调试与排查

  • 利用 LoggingJobHistoryPlugin 记录每次作业执行历史
  • 监听器实现自定义日志和告警
  • 第三方工具如 QuartzminQuartz.NetUI(基于 Vue 的定时任务管理系统)提供可视化任务管理

4. 动态作业管理

  • 将作业配置存储在数据库或配置中心,通过代码动态注册而非硬编码
  • 利用 IScheduler 接口实现运行时的暂停/恢复/删除
  • 慎用 StartNow() 方法,避免应用启动时大量任务同时执行

十一、资源与扩展

相关推荐
我是唐青枫2 小时前
C#.NET ThreadLocal 深入解析:线程独享数据、性能收益与实战边界
c#·.net
唐青枫6 小时前
别再把增删改查写成一锅粥!C#.NET CQRS 从原理到实战
c#·.net
唐青枫17 小时前
C#.NET ThreadLocal 深入解析:线程独享数据、性能收益与实战边界
c#·.net
SEO-狼术1 天前
Include Scannable Barcodes in Reports
.net
qq_431280701 天前
工作经验总结:半导体上位机软件开发与互联网开发的不同
c#·.net
ironinfo1 天前
.net 高并发服务性能瓶颈排查处理
性能优化·.net·grpc
回忆2012初秋1 天前
【.Net】一文讲清楚SonnetDB 时序库的使用
.net
雪飞鸿2 天前
ArrayPoolWrapper简洁、安全的ArrayPool
c#·.net·.net core·原创