Quartz
Quartz 是一个强大且可靠的开源调度库,可用于在 .NET 应用程序中执行定时任务。它可以让您在预定的时间自动触发任务,以便您可以集中精力于开发其他方面。它支持循环调度、延迟调度、并行执行等高级调度功能,并且具有可扩展性和高度定制化能力。
# 安装
您可以通过 NuGet 包管理器或手动下载安装 Quartz。最新的稳定版本为 Quartz 3.7.0。
要在 Visual Studio 中安装 Quartz,请打开 NuGet 包管理器控制台,并运行以下命令:
PM> Install-Package Quartz
# 简介
Quartz 是一个调度框架,其核心是调度器(Scheduler)。Quartz 调度器负责调度所有计划的任务,并在任务需要执行时运行它们。任务由作业(Job)表示,作业是由可执行代码组成的类。在执行时,作业将被 Quartz 调度器委托给作业执行器(Job Executor)来运行。作业执行器是一个负责从作业中调用特定方法的组件。
# 作业
作业是 Quartz 中执行工作的基本单元。它们表示可执行代码的逻辑单元,并且由调度程序定期触发。作业可以实现 IJob
接口,该接口定义了一个执行方法 Execute
,在每次调度时会调用该方法。下面是一个简单的作业示例:
public class MyJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// 执行作业逻辑
}
}
# 触发器
触发器(Trigger)是 Quartz 中的计划单元,它指定何时触发作业。每个作业可以有多个触发器,每个触发器都有一个名称和一个与其相关联的作业。 Quartz 支持多种触发器类型,包括 SimpleTrigger、CronTrigger、CalendarIntervalTrigger 等。下面是一个简单的触发器示例:
// 每分钟触发一次作业
var trigger = TriggerBuilder.Create()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(1).RepeatForever())
.Build();
# 调度器
调度器(Scheduler)是 Quartz 中的核心组件,它负责管理作业和触发器,并根据其计划运行作业。每个应用程序只需要一个调度器实例,该实例可以在整个应用程序生命周期中重复使用。下面是一个创建调度器示例:
var schedulerFactory = new StdSchedulerFactory();
var scheduler = await schedulerFactory.GetScheduler();
await scheduler.Start();
# 任务调度
一旦有了作业、触发器和调度器,就可以开始安排任务调度了。在 Quartz 中,任务调度的流程如下:
- 创建一个作业实例。
- 创建一个触发器实例。
- 将作业和触发器关联起来。
- 将作业和触发器注册到调度器中。
下面是一个简单的任务调度示例,它使用 SimpleTrigger 触发器每分钟调度一个作业:
// 创建作业
var jobDetail = JobBuilder.Create<MyJob>()
.WithIdentity("job1", "group1")
.Build();
// 创建触发器
var trigger = TriggerBuilder.Create()
.WithIdentity("trigger1", "group1")
.WithSimpleSchedule(x => x.WithIntervalInMinutes(1).RepeatForever())
.Build();
// 将作业和触发器注册到调度器中
await scheduler.ScheduleJob(jobDetail, trigger);
在上面的示例中,作业名称为 "job1",所属分组为 "group1"。触发器名称为 "trigger1",所属分组为 "group1"。SimpleSchedule 触发器定义了每分钟调度作业一次,并且永久重复执行。
# 延迟调度
除了简单的定时调度外,Quartz 还支持延迟调度。延迟调度是指等待一定时间后再执行任务。在 Quartz 中,可以使用 SimpleTrigger 触发器来实现延迟调度。下面是一个延迟调度示例:
// 创建作业
var jobDetail = JobBuilder.Create<MyJob>()
.WithIdentity("job1", "group1")
.Build();
// 创建触发器
var trigger = TriggerBuilder.Create()
.WithIdentity("trigger1", "group1")
.StartAt(DateTimeOffset.UtcNow.AddMinutes(5)) // 延迟 5 分钟后执行
.WithSimpleSchedule(x => x.WithIntervalInMinutes(1).RepeatForever())
.Build();
// 将作业和触发器注册到调度器中
await scheduler.ScheduleJob(jobDetail, trigger);
在上面的示例中,使用 StartAt
方法来指定触发器的开始时间,从而实现延迟调度。此处设置了触发器在当前时间的 5 分钟后开始调度作业。注意,需要使用 DateTimeOffset.UtcNow
来获取当前时间。
# 循环调度
除了简单的定时调度和延迟调度外,Quartz 还支持循环调度。循环调度是指定期重复执行任务,每次执行完成后等待一定时间后再执行。在 Quartz 中,可以使用 SimpleTrigger 触发器来实现循环调度。下面是一个循环调度示例:
// 创建作业
var jobDetail = JobBuilder.Create<MyJob>()
.WithIdentity("job1", "group1")
.Build();
// 创建触发器
var trigger = TriggerBuilder.Create()
.WithIdentity("trigger1", "group1")
.WithSimpleSchedule(x => x.WithIntervalInSeconds(10).RepeatForever())
.Build();
// 将作业和触发器注册到调度器中
await scheduler.ScheduleJob(jobDetail, trigger);
在上面的示例中,使用 WithIntervalInSeconds
方法来指定触发器的时间间隔为 10 秒。RepeatForever
方法指定了触发器将永久重复执行。这将导致作业每隔 10 秒重复执行一次,直到调度器停止。
# 并行串行执行
Quartz 支持并行执行多个作业。作业可以使用 DisallowConcurrentExecution
特性来指示调度程序不允许同一作业实例在同一时间执行。这样可以确保不同实例的作业在并行执行时互不干扰。
例如:一个任务,我们定义5秒运行一次,但是运行过程可能会比较长(例如要12秒才可以计算完成),这样就会造成前一个任务还没有执行完毕,后一个新任务又启动了(这样会造成多个任务并行在执行)
如果在类上标注:[DisallowConcurrentExecution],这样新任务启动时,必须在前一个任务已经完成的情况下(这样任务是一个接一个的,是串行的) 以本demo来说:12秒后才会启动一个新任务,任务和任务不会并行(当然任务与任务之间的间隔就不是原有的5秒了)
下面是一个并行执行示例:
[DisallowConcurrentExecution]
public class MyJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// 执行作业逻辑
//故意停顿12秒
System.Threading.Thread.Sleep( 12000 );
}
}
在上面的示例中,使用 DisallowConcurrentExecution
特性来指示调度程序不允许同一作业实例在同一时间执行。这将确保在并行执行作业时不会发生竞争条件。
# .net core中使用
# 1.安装
Quartz.AspNetCore .net core3.1及之后版本安装这个
Quartz.Extensions.Hosting 之前版本安装这个
# 2.定义一个类实现接口IJob
public class HelloWorldJob : IJob
{
private readonly ILogger<HelloWorldJob> _logger;
public HelloWorldJob ( ILogger<HelloWorldJob> logger )
{
_logger = logger;
}
public Task Execute ( IJobExecutionContext context )
{
Console.WriteLine( "Execute" );
_logger.LogInformation( "Hello world!" );
return Task.CompletedTask;
}
}
# 3.注册
.net core 3.1版本
public void ConfigureServices(IServiceCollection services)
{
services.AddQuartz( q =>
{
// Use a Scoped container to create jobs. I'll touch on this later
//UseMicrosoftDependencyInjectionJobFactory(),这个地方告诉Quartz.NET注册一个IJobFactory,然后从DI容器中获取Job,这样也可以使用 Scoped 类型的服务。
q.UseMicrosoftDependencyInjectionJobFactory();
var jobKey = new JobKey( "SampleJob1" , "JobGroup1" );
q.AddJob<HelloWorldJob>( opts => opts.WithIdentity( jobKey ) );
// Create a trigger for the job
q.AddTrigger( opts => opts
.ForJob( jobKey ) // link to the Job
.WithIdentity( "HelloWorldJob-trigger" , "TGroup1" ) // give the trigger a unique name
// 传递参数过去
.UsingJobData( "par1" , 12 )
.UsingJobData( "par2" , "qq" )
//.StartAt( DateBuilder.EvenSecondDate( DateTimeOffset.UtcNow.AddSeconds( 5 ) ) )
// .StartNow()
// 下面这个表达式,可以考虑配置到配置文件中
.WithCronSchedule( "0/15 * * * * ?" ) ); // run every 5 seconds
} );
// ASP.NET Core hosting
services.AddQuartzServer(options =>
{
// when shutting down we want jobs to complete gracefully
// WaitForJobsToComplete():当程序关闭时,此设置可确保http://Quartz.NET在退出之前等待Job正常结束。
options.WaitForJobsToComplete = true;
});
}
.net6版本
builder.Services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionScopedJobFactory();
// Just use the name of your job that you created in the Jobs folder.
var jobKey = new JobKey("SendEmailJob");
q.AddJob<SendEmailJob>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("SendEmailJob-trigger")
//This Cron interval can be described as "run every minute" (when second is zero)
.WithCronSchedule("0 * * ? * *")
);
});
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
# 高级功能
除了上面介绍的基本功能外,Quartz 还支持许多高级功能。下面是一些常见的高级功能:
# 传递参数
# a.传递参数
# 1.利用Job传递参数
1.传递键值参数 2.利用JobDataMap可以传递对象类型数据
//这里演示了2种传递参数的方式:1种是JobDataMap(如果需要传递对象,可以这样做),1种是键值
IJobDetail job4 = JobBuilder.Create<RunClassA>().WithIdentity( "Job4" , "JobGroup4" )
.UsingJobData( getMyData() )
.UsingJobData( "my3" , 110f )
.Build();
//这样,也可以传递参数
job4.JobDataMap.Add( "my4" , "123qq" );
job4.JobDataMap.Add( "my5" , true );
/// <summary>
/// 返回JobDataMap,传递类
/// </summary>
/// <returns></returns>
JobDataMap getMyData ()
{
DateTime now = DateTime.Now;
System.Collections.Generic.Dictionary<string , MyClass> mydatas = new Dictionary<string , MyClass>();
mydatas.Add( "my1" , new MyClass()
{
MyName = "qq1" ,
Times = now
} );
mydatas.Add( "my2" , new MyClass()
{
MyName = "sina2" ,
Times = now
} );
return new JobDataMap( mydatas );
}
# 2.利用Trigger传递参数
Trigger中传递参数的方式和job中是一样的
ITrigger trigger4 = TriggerBuilder.Create().WithIdentity( "_Trigger4" , "TGroup4" )
.WithCronSchedule( cron_4 )
//在Trigger中也可以传递参数的
.UsingJobData( "t1" , "abc" )
.Build();
// 这样,也可以传递参数
trigger4.JobDataMap.Add("t2",123);
# b.接收参数
# 1.接收Job传递参数
//接收传递过来参数
JobDataMap dataMap = context.JobDetail.JobDataMap;
//接收的参数值是一直不变的
float f1 = dataMap.GetFloat( "my3" );
string my4 = dataMap.GetString( "my4" );
bool my5 = dataMap.GetBoolean( "my5" );
//dataMap.Get可以接收一个类型数据
MyClass my1Data = ( MyClass ) dataMap.Get( "my1" );
MyClass my2Data = ( MyClass ) dataMap.Get( "my2" );
# 2.接收Trigger传递参数
//接收Trigger中传递参数
JobDataMap triJobDataMap = context.Trigger.JobDataMap;
var val = triJobDataMap.GetString( "t1" );
int t2 = triJobDataMap.GetInt( "t2" );
# c.参数传递问题
有时我们想把参数改变后,传递给下一个任务使用,可以使用PersistJobDataAfterExecution特性,并且可以加上DisallowConcurrentExecution特性
PersistJobDataAfterExecution特性就是JobDataMap中的数据被改动, 可以给下一次调用时用
DisallowConcurrentExecution特性是禁止相同JobDetail同时执行,也就是禁止并行任务,必须任务一个个进行(串行的)
[DisallowConcurrentExecution]
[PersistJobDataAfterExecution]
public class RunClassB : Quartz.IJob
{
public void Execute ( Quartz.IJobExecutionContext context )
{
i = i + 1;
JobKey key = context.JobDetail.Key;
var trikey = context.Trigger.Key;
//接收传递过来参数
JobDataMap dataMap = context.JobDetail.JobDataMap;
//接收的参数值是一直不变的
MyClass my1Data = ( MyClass ) dataMap.Get( "my1" );
MyClass my2Data = ( MyClass ) dataMap.Get( "my2" );
float f1 = dataMap.GetFloat( "my3" );
//context.JobDetail.JobDataMap.Put( "my3" , i );
//修改参数值, 保存参数 (下个任务进入就可以取到这个值), 记得 给类标注 [PersistJobDataAfterExecution] ,要不下次任务取到的还是初始的那个值,不是这个累加值
dataMap.Put( "my3" , f1 + i );
//接收Trigger中传递参数
JobDataMap triJobDataMap = context.Trigger.JobDataMap;
var val = triJobDataMap.GetInt( "t1" );
// 修改参数值, 保存参数 (下个任务进入就可以取到这个值), 记得 给类标注 [PersistJobDataAfterExecution] ,要不下次任务取到的还是初始的那个值,不是这个累加值
context.Trigger.JobDataMap.Put( "t1" , val + i );
}
}
# 监听器
Quartz 支持监听器,您可以使用它来处理任务调度期间的事件。您可以使用 IJobListener 和 ITriggerListener 接口实现自定义监听器。下面是一个简单的监听器示例:
public class MyJobListener : IJobListener
{
public string Name => "MyJobListener";
public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
在上面的示例中,实现了 IJobListener 接口的自定义监听器 MyJobListener
,它可以在作业执行前、执行后或作业被否决执行时执行自定义逻辑。
# 连接数据库
Quartz 可以使用多种数据库来存储作业和触发器的状态。Quartz 提供了许多适配器,可以轻松地将其与常见的关系数据库集成。下面是一个使用 SQL Server 存储作业和触发器状态的示例:
var schedulerFactory = new StdSchedulerFactory(properties);
var scheduler = await schedulerFactory.GetScheduler();
// 使用 SQL Server 存储作业和触发器状态
var jobStore = new AdoJobStore();
jobStore.Initialize(new NameValueCollection
{
["connectionString"] = "Data Source=myServerAddress;Initial Catalog=myDataBase;User Id=myUsername;Password=myPassword;",
["driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["tablePrefix"] = "QRTZ_"
});
scheduler.JobStore = jobStore;
await scheduler.Start();
```csharp
在上面的示例中,使用 `AdoJobStore` 存储作业和触发器状态。通过 `connectionString` 参数指定 SQL Server 数据库连接字符串,`driverDelegateType` 指定使用的数据库适配器,`tablePrefix` 指定表名前缀。
### 集群模式
Quartz 支持集群模式,多个调度器可以共享作业和触发器。集群模式使用数据库锁来协调调度器之间的作业和触发器,从而确保每个作业和触发器在集群中只有一个调度器可以使用。要启用集群模式,需要做以下几个步骤:
1. 将多个调度器连接到同一个数据库。
2. 将作业和触发器的状态存储在数据库中。
3. 在调度器中启用集群模式。
下面是一个启用集群模式的示例:
```csharp
var schedulerFactory = new StdSchedulerFactory(properties);
var scheduler = await schedulerFactory.GetScheduler();
// 将作业和触发器的状态存储在数据库中
var jobStore = new AdoJobStore();
jobStore.Initialize(new NameValueCollection
{
["connectionString"] = "Data Source=myServerAddress;Initial Catalog=myDataBase;User Id=myUsername;Password=myPassword;",
["driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["tablePrefix"] = "QRTZ_"
});
scheduler.JobStore = jobStore;
// 启用集群模式
var instanceId = "instance1";
scheduler.Context.Add("InstanceId", instanceId);
scheduler.Start();
// 添加更多实例
var scheduler2 = await schedulerFactory.GetScheduler();
var instanceId2 = "instance2";
scheduler2.Context.Add("InstanceId", instanceId2);
await scheduler2.Start();
在上面的示例中,使用 AdoJobStore
存储作业和触发器状态,并启用了集群模式。使用 Context
属性添加一个实例 ID,以便在集群中识别不同的调度器实例。在这个示例中,创建了两个调度器实例,分别为 "instance1" 和 "instance2"。
# 总结
Quartz 是一个强大且可靠的开源调度库,可用于在 .NET 应用程序中执行定时任务。它支持循环调度、延迟调度、并行执行等高级调度功能,并且具有可扩展性和高度定制化能力。在本文中,我们介绍了 Quartz 的基本概念和用法,并演示了如何使用 Quartz 进行任务调度。我们还介绍了一些高级功能,例如监听器、数据库连接和集群模式。通过学习本文,您应该能够掌握 Quartz 的基本概念和用法,并了解一些高级功能,以便更好地应用 Quartz 在您的项目中进行任务调度。