1. 关于Quartz.Net
Quartz.NET是一个Github开源的作业调度系统。
如官网简介👇。

官方文档:Quartz 3 Quick Start | Quartz.NET (quartz-scheduler.net)
2. 快速安装
启动VS进入项目(为了演示效果,这里新建了一个基于.NET7的控制台应用项目)
右键打开Nuget管理界面,搜索Quartz安装即可。
目前最新版本V3.9.0👇

3. 快速创建一个定时任务
首先来创建一个简单的定时任务JobA。
继承自Quertz提供的基类IJob,实现Execute方法,输出字符串。
public class JobA : IJob
{
public async Task Execute(IJobExecutionContext context)
{
await Console.Out.WriteLineAsync($"AAAAAAA_{DateTime.Now}_Hello!");
}
}
接下来,初始化Job,并配置Job对应的触发器。
当满足触发器配置的参数条件, 该Job将被触发。
//初始化Job
IJobDetail jobA = JobBuilder.Create<JobA>()
.WithIdentity("JobA", "AAAA")
.Build();
//初始化对应触发器
ITrigger triggerJobA = TriggerBuilder.Create()
.WithIdentity("TriggerJobA", "AAAA")
.WithSimpleSchedule(x =>
{
x.WithIntervalInSeconds(3).RepeatForever();
})
.Build();
最后,添加任务调度。将Job和触发器添加到任务调度中。
//创建任务调度器
IScheduler scheduler = await factory.GetScheduler();
//启动任务调度器
scheduler.Start();
//将任务和触发器添加到任务调度器中
scheduler.ScheduleJob(jobA, triggerJobA);
运行结果:

由此我们可以得出 —— 快速配置并执行一个定时任务,通需要完成3个步骤:
- 创建Job和JobDetail。
- 创建触发器。触发器记录了作业运行的条件,只有条件被满足,作业才会执行。
- 创建任务调度器并添加调度。调度器通常在应用程序启动时创建,一个应用程序实例通常创建一个调度器即可。

4. Job作业
4.1 Job和JobData
Job,即作业类型,描述了作业是如何执行的。
它由用户自定义,必须继承IJob接口,需要实现IJob接口中的Execute方法。
方法的唯一参数context中包含与当前上下文中关联的Scheduler、JobDetail、Trigger等。
JobData,是Job类中包含的成员属性,可以在初始化Job时传入,在Job运行时使用。
比如如下实例:
创建Job和JobData
public class JobB : IJob
{
//JobData
public string UserCode { get; set; }
public string UserName { get; set; }
public async Task Execute(IJobExecutionContext context)
{
await Console.Out.WriteLineAsync($"BBBBBB_{DateTime.Now}__World!{UserCode}+{UserName}");
}
}
初始化Job,在jobDetail中通过SetJobData方法传入JobDataMap对象,用以传递参数
IJobDetail jobB = JobBuilder.Create<JobB>()
.SetJobData(new JobDataMap() {
new KeyValuePair<string, object>("UserCode", "Code001"),
new KeyValuePair<string, object>("UserName", "Tom")
})
.WithIdentity("JobB", "BBBB")
.Build();
4.2 JobDetail
JobDetail是Quartz对作业的封装。
它包含Job类型,Job在执行时用到的数据,以及是否要持久化、是否覆盖已存在的作业等选项。
JobDetail需要通过JobBuilder来创建。
代码如下:
IJobDetail jobB = JobBuilder.Create<JobB>()
.SetJobData(new JobDataMap() {
new KeyValuePair<string, object>("UserCode", "Code001"),
new KeyValuePair<string, object>("UserName", "Tom")
})
.StoreDurably(true)
.RequestRecovery(true)
.WithDescription("hello world jobB")
.WithIdentity("JobB", "BBBB")
.Build();
SetJobData:设置JobDataStoreDurably:孤立存储,指即使该JobDetail没有关联的Trigger,也会进行存储RequestRecovery:请求恢复,指应用崩溃后再次启动,会重新执行该作业WithIdentity:作业的唯一标识WithDescription:作业的描述信息
除此之外,Quartz.Net还支持两个非常有用的特性:
DisallowConcurrentExecution:禁止并行执行,该特性是针对JobDetail生效的PersistJobDataAfterExecution:在执行完成后持久化JobData,该特性是针对Job类型生效的,意味着所有使用该Job的JobDetail都会在执行完成后持久化JobData。
4.3 持久化JobData
演示一下PersistJobDataAfterExecution特性。
添加特性,设置在运行时更新JobData。
[PersistJobDataAfterExecution]
public class JobC : IJob
{
public string FlgVal { get; set; }
public async Task Execute(IJobExecutionContext context)
{
await Console.Out.WriteLineAsync($"CCCCCC_{DateTime.Now}__Hello World!!!!!!!! Flg={FlgVal}");
context.JobDetail.JobDataMap.Put("FlgVal", "xixixi!");
}
}
初始化JobData,添加任务调度。
IJobDetail jobC = JobBuilder.Create<JobC>()
.SetJobData(new JobDataMap() {
new KeyValuePair<string, object>("FlgVal", "hahaha!"),
})
.WithIdentity("JobC", "CCCC")
.Build();
ITrigger triggerJobC = TriggerBuilder.Create()
.WithIdentity("TriggerJobC", "CCCC")
.WithSimpleSchedule(x =>
{
x.WithIntervalInSeconds(3).RepeatForever();
})
.Build();
运行结果:

5. Trigger触发器
触发器对象用于触发作业的执行(或“触发”)。当您希望计划作业时,可以实例化触发器并使用其属性来配置您希望拥有的计划。
Quartz中常用的触发器有Sample和Cron两种。
5.1 SampleTrigger
顾名思义,这是个简单的触发器,有以下特性:
- 重复执行:WithRepeatCount()/RepeatForever()
- 设置间隔时间:WithInterval()
- 定时执行:StartAt()/StartNow()
- 设定优先级:WithPriority(),默认为5
需要注意:当Trigger到达StartAt指定的时间时会执行一次,这一次执行是不包含在WithRepeatCount中的。在我们上面的例子中可以看出,添加调度后会立即执行一次,然后重复三次,最终执行了四次。
5.2 CronTrigger
CronTrigger是通过Cron表达式来完成调度的。Cron表达式非常灵活,可以实现几乎各种定时场景的需要。
关于Cron表达式,大家可以移步 Quartz Cron表达式
使用CronTrigger的示例如下:
var trigger = TriggerBuilder.Create()
.WithCronSchedule("*/1 * * * * ?")
.Build();
5.3 日历Calender
Calendar可以与Trigger进行关联,从Trigger中排出执行计划。例如你只希望在工作日执行作业,那么我们可以定义一个休息日的日历,将它与Trigger关联,从而排出休息日的执行计划。
Calendar示例代码如下:
var calandar = new HolidayCalendar();
calandar.AddExcludedDate(DateTime.Today);
await scheduler.AddCalendar("holidayCalendar", calandar, false, false);
var trigger = TriggerBuilder.Create()
.WithCronSchedule("*/1 * * * * ?")
.ModifiedByCalendar("holidayCalendar")
.Build();
在这个示例中,我们创建了HolidayCalendar日历,然后添加排除执行的日期。我们把今天添加到排除日期后,该Trigger今天将不会触发。
6. 3种监听器
监听器用于在运行时获取作业状态、处理作业数据等。它可以根据运行中发生的事件执行相应操作。
大多数 Quartz.NET 用户不使用侦听器,但当应用程序需求需要事件通知时,而作业本身无需显式通知应用程序,侦听器非常方便。
6.1 作业监听器 JobListeners
PS. 必须确保触发器和作业监听器永远不会引发异常(使用 try-catch),并且它们可以处理内部问题。当监听器通知失败时,Quartz 无法确定监听器中所需的逻辑是否成功完成,作业可能会卡住。
JobListener可以监听Job执行前、执行后、否决执行的事件。
需要继承IJobListener接口。
public interface IJobListener
{
string Name { get; }
Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default);
Task JobExecutionVetoed(IJobExecutionContext context,CancellationToken cancellationToken = default);
Task JobWasExecuted(IJobExecutionContext context,JobExecutionException? jobException,CancellationToken cancellationToken = default);
}
实现代码实现如下:
/// <summary>
/// 作业监听器
/// </summary>
public class DemoJobListener : IJobListener
{
public string Name { get; } = nameof(DemoJobListener);
/// <summary>
/// Job执行前
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
return Task.Factory.StartNew(() =>
{
Console.WriteLine($"Job: {context.JobDetail.Key} 即将执行");
});
}
/// <summary>
/// Job否决执行
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
return Task.Factory.StartNew(() =>
{
Console.WriteLine($"Job: {context.JobDetail.Key} 被否决执行");
});
}
/// <summary>
/// Job执行后
/// </summary>
/// <param name="context"></param>
/// <param name="jobException"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken = default)
{
return Task.Factory.StartNew(() =>
{
Console.WriteLine($"Job: {context.JobDetail.Key} 执行完成");
});
}
}
注册Job监听器:
//将Job监听器添加到Scheduler中
scheduler.ListenerManager.AddJobListener(new DemoJobListener(), GroupMatcher<JobKey>.AnyGroup());
> 监听器在运行时向调度程序注册,**并且不会**与作业和触发器一起存储在 JobStore 中。这是因为监听器通常是应用程序的集成点。因此,每次应用程序运行时,都需要向调度程序重新注册监听器。
执行结果:

6.2 触发器监听器 TriggerListeners
TriggerListener可以监听Trigger的执行情况。需要继承ITriggerListener接口。
public interface ITriggerListener
{
string Name { get; }
Task TriggerFired(ITrigger trigger, IJobExecutionContext context);
Task<bool> VetoJobExecution(ITrigger trigger, IJobExecutionContext context);
Task TriggerMisfired(ITrigger trigger);
Task TriggerComplete(ITrigger trigger, IJobExecutionContext context, int triggerInstructionCode);
}
实现代码如下:
/// <summary>
/// 触发器监听器
/// </summary>
public class DemoTriggerListener : ITriggerListener
{
public string Name { get; } = nameof(DemoTriggerListener);
public Task TriggerComplete(ITrigger trigger, IJobExecutionContext context, SchedulerInstruction triggerInstructionCode, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task TriggerFired(ITrigger trigger, IJobExecutionContext context, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task TriggerMisfired(ITrigger trigger, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task<bool> VetoJobExecution(ITrigger trigger, IJobExecutionContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult(true); //返回true表示否决Job继续执行
}
}
注册Trigger监听器:
scheduler.ListenerManager.AddTriggerListener(new DemoTriggerListener(), GroupMatcher<TriggerKey>.AnyGroup());
6.3 调度监听器 SchedulerListeners
PS.确保调度程序侦听器从不引发异常(使用 try-catch),并且它们可以处理内部问题。当侦听器通知失败时,当 Quartz 无法确定侦听器中所需的逻辑是否成功完成时,它可能会处于不可预测的状态。
SchedulerListener提供了Job、Trigger管理的监听,
与调度程序相关的事件包括:添加作业/触发器、删除作业/触发器、调度程序中的严重错误、调度程序被关闭的通知等。
需要继承ISchedulerListener接口:
public interface ISchedulerListener
{
Task JobScheduled(Trigger trigger);
Task JobUnscheduled(string triggerName, string triggerGroup);
Task TriggerFinalized(Trigger trigger);
Task TriggersPaused(string triggerName, string triggerGroup);
Task TriggersResumed(string triggerName, string triggerGroup);
Task JobsPaused(string jobName, string jobGroup);
Task JobsResumed(string jobName, string jobGroup);
Task SchedulerError(string msg, SchedulerException cause);
Task SchedulerShutdown();
}
添加 SchedulerListener:
scheduler.ListenerManager.AddSchedulerListener(mySchedListener);
删除 SchedulerListener:
scheduler.ListenerManager.RemoveSchedulerListener(mySchedListener);
6.4 监听器的多种覆盖方式
添加对特定作业感兴趣的 JobListener:
scheduler.ListenerManager.AddJobListener(myJobListener, KeyMatcher<JobKey>.KeyEquals(new JobKey("myJobName", "myJobGroup")));
添加对特定组的所有作业感兴趣的 JobListener:
scheduler.ListenerManager.AddJobListener(myJobListener, GroupMatcher<JobKey>.GroupEquals("myJobGroup"));
添加对两个特定组的所有作业感兴趣的 JobListener:
scheduler.ListenerManager.AddJobListener(myJobListener,
OrMatcher<JobKey>.Or(GroupMatcher<JobKey>.GroupEquals("myJobGroup"), GroupMatcher<JobKey>.GroupEquals("yourGroup")));
添加对所有作业感兴趣的 JobListener:
scheduler.ListenerManager.AddJobListener(myJobListener, GroupMatcher<JobKey>.AnyGroup());
7. 持久化 JobStore
Quartz.Net支持Job的持久化操作,被称为JobStore。
默认情况下,Quartz将数据持久化到内存中,好处是内存的速度很快,坏处是无法提供负载均衡的支持,并且在程序崩溃后,我们将丢失所有Job数据,对于企业级系统来说,坏处明显大于好处,因此有必要将数据存储在数据库中。
Quartz使用ADO.NET访问数据库,支持的数据库厂商非常广泛:
- SqlServer - .NET Framework 2.0的SQL Server驱动程序
- OracleODP - Oracle的Oracle驱动程序
- OracleODPManaged - Oracle的Oracle 11托管驱动程序
- MySql - MySQL Connector / .NET
- SQLite - SQLite ADO.NET Provider
- SQLite-Microsoft - Microsoft SQLite ADO.NET Provider
- Firebird - Firebird ADO.NET提供程序
- Npgsql - PostgreSQL Npgsql
数据库的创建语句可以在Quartz.Net的源码中找到:github.com/quartznet/q…
我们可以通过配置文件来配置Quartz使用数据库存储:
# job store
quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz
quartz.jobStore.dataSource = quartz_store
quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.PostgreSQLDelegate, Quartz
#quartz.jobStore.useProperties = true
quartz.dataSource.quartz_store.connectionString = Server=localhost;Database=quartz_store;userid=quartz_net;password=xxxxxx;Pooling=true;MinPoolSize=1;MaxPoolSize=10;Timeout=15;SslMode=Disable;
quartz.dataSource.quartz_store.provider = Npgsql
8. 负载均衡
负载均衡是实现高可用的一种方式,当任务量变大以后,单台服务器很难满足需要,使用负载均衡则使得系统具备了横向扩展的能力,通过部署多个节点来增加处理Job的能力。
Quartz.Net在使用负载均衡时,需要依赖ADO JobStore,意味着你需要使用数据库持久化数据。然后我们可以使用以下配置完成负载均衡功能:
quartz.jobStore.clustered = true
quartz.scheduler.instanceId = AUTO
- clustered:集群的标识
- instanceId:当前Scheduler实例的ID,每个示例的ID不能重复,使用AUTO时系统会自动生成ID
当我们在多台服务器上运行Scheduler实例时,需要设置服务器的时钟时间,确保服务器时间是相同的。针对windows服务器,可以设置从网络自动同步时间。
9. 通过Routing访问Quartz实例
通过Routing访问Quartz实例的功能,为我们做系统分离提供了很好的途径。
我们可以通过以下配置实现Quartz的服务器端远程访问:
# export this server to remoting context
quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz
quartz.scheduler.exporter.port = 555
quartz.scheduler.exporter.bindName = QuartzScheduler
quartz.scheduler.exporter.channelType = tcp
quartz.scheduler.exporter.channelName = httpQuartz
然后我们在客户端系统中配置访问:
quartz.scheduler.proxy = true
quartz.scheduler.proxy.address = tcp://localhost:555/QuartzScheduler