Quartz.Net使用

1,258 阅读5分钟

1. 关于Quartz.Net

Quartz.NET是一个Github开源的作业调度系统。

如官网简介👇。

image.png

官方文档:Quartz 3 Quick Start | Quartz.NET (quartz-scheduler.net)

感谢大佬分享:Quartz.Net使用教程 - 拓荒者FF - 博客园 (cnblogs.com)


2. 快速安装

启动VS进入项目(为了演示效果,这里新建了一个基于.NET7的控制台应用项目)

右键打开Nuget管理界面,搜索Quartz安装即可。

目前最新版本V3.9.0👇

image.png


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);

运行结果:

image.png

由此我们可以得出 —— 快速配置并执行一个定时任务,通需要完成3个步骤:

  1. 创建Job和JobDetail。
  2. 创建触发器。触发器记录了作业运行的条件,只有条件被满足,作业才会执行。
  3. 创建任务调度器并添加调度。调度器通常在应用程序启动时创建,一个应用程序实例通常创建一个调度器即可。

image.png


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:设置JobData
  • StoreDurably:孤立存储,指即使该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();

运行结果:

image.png


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 中。这是因为监听器通常是应用程序的集成点。因此,每次应用程序运行时,都需要向调度程序重新注册监听器。

执行结果: image.png

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