对简单的Quartz了解的不简单一些

Quartz.Net的关键接口

  • Scheduler - 与调度程序交互的主要API。[ IScheduler]
  • Job - 由希望由调度程序执行的组件实现的接口。[IJob]
  • JobDetail - 用于定义作业的实例。[IJobDetail]
  • Trigger(即触发器) - 定义执行给定作业的计划的组件。[ITrigger]
  • JobBuilder - 用于定义/构建JobDetail实例,用于定义作业的实例。
  • TriggerBuilder - 用于定义/构建触发器实例。
    public class HelloJob : IJob
    {
        public Task Execute(IJobExecutionContext context)
        {
            Console.WriteLine("Hello World");
            return Task.CompletedTask;
        }
    }
    
    class Program
    {
        static async Task Main(string[] args)
        {
            StdSchedulerFactory factory = new StdSchedulerFactory();
            var scheduler = await factory.GetScheduler();
            await scheduler.Start();
            // define the job and tie it to our HelloJob class
            IJobDetail job = JobBuilder.Create<HelloJob>()
                .WithIdentity("myJob", "group1")
                .Build();

            // Trigger the job to run now, and then every 3 seconds
            ITrigger trigger = TriggerBuilder.Create()
                .WithIdentity("myTrigger", "group1")
                .StartNow()
                .WithSimpleSchedule(x => x
                    .WithIntervalInSeconds(3)
                    .RepeatForever())
            .Build();
            await scheduler.ScheduleJob(job, trigger);

            // 30秒后停止调度计划
            await Task.Delay(1000 * 30);
            await scheduler.Shutdown();
        }
    }

Scheduler

Scheduler的生命期,从SchedulerFactory创建它时开始,到Scheduler调用Shutdown()方法时结束。

Scheduler被创建后,可以增加、删除和列举Job和Trigger,以及执行其它与调度相关的操作(如暂停Trigger)。

Scheduler只有在调用Start()方法后,才会真正地触发trigger 。

Job与JobDetail

下面讲到的 Job 都是指的是实现 IJob 的类,例如:HelloJob

  • JobDetail

    1. JobDetail实例是通过JobBuilder类创建的。
        // define the job and tie it to our HelloJob class
        IJobDetail job = JobBuilder.Create<HelloJob>()
            .WithIdentity("myJob", "group1")
            .Build();
    
    1. 注册到 Scheduler 的不是Job对象,而是 JobDetail 实例 。Job对象只是 JobDetail 实例的一部分。
        await scheduler.ScheduleJob(job, trigger);
    

    可以只创建一个job类,然后创建多个与该Job关联的JobDetail实例,每一个实例都有自己的属性集和JobDataMap,最后,将所有的实例都加到scheduler中 。

    1. 通过JobDetail对象,可以给job实例配置的其它属性有:

      Durability:[StoreDurably()]如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的存在与否决定的;

      RequestsRecovery:[RequestRecovery()]如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。

  • Job

    1. 每一个Job都必须实现IJob。例如上面的 HelloJob。这个类仅仅表明该job需要完成什么类型的任务,除此之外,Quartz还需要知道该Job实例所包含的属性,这将由JobDetail类来完成。

          public class HelloJob : IJob
          {
              public Task Execute(IJobExecutionContext context)
              {
                  Console.WriteLine("Hello World");
                  return Task.CompletedTask;
              }
          }
      
    2. Job的生命周期

        // define the job and tie it to our HelloJob class
        IJobDetail job = JobBuilder.Create<HelloJob>()
            .WithIdentity("myJob", "group1")
            .Build();
    
        // Trigger the job to run now, and then every 40 seconds
        ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity("myTrigger", "group1")
            .StartNow()
            .WithSimpleSchedule(x => x
                .WithIntervalInSeconds(3)
                .RepeatForever())
        	.Build();
        await scheduler.ScheduleJob(job, trigger);
    

    可以看到,我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名(HelloJob)传给了JobDetail,所以scheduler就知道了要执行何种类型的job;

    每次当scheduler执行job时,在调用其Execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收。

    Job 的实例要到该执行它们的时候才会实例化出来。每次 Job 被执行,一个新的 Job 实例会被创建。也就是说 Job 不必担心线程安全性,因为同一时刻仅有一个线程去执行给定 Job 类的实例,甚至是并发执行同一 Job 也是如此。

    这种执行策略需要我们注意:
    
    * job必须有一个无参的构造函数;
    * 在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留。 
    

    那么如何给job实例增加属性或配置呢?如何在job的多次执行中,跟踪job的状态呢?答案就是:JobDataMap,JobDetail对象的一部分。 (后面会详细介绍用法)

Trigger

Trigger对象是用来触发执行Job的。当调度一个job时,我们实例一个触发器然后调整它的属性来满足job执行的条件。触发器也有一个和它相关的JobDataMap,它是用来给被触发器触发的job传参数的。

  1. SimpleTrigger可以满足的调度需求是:在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。

    	ITrigger trigger = TriggerBuilder.Create()
    		.WithIdentity("myTrigger", "group1")
    		.StartNow()
    		.WithPriority(5)
    		.WithSimpleSchedule(x => x
    			.WithIntervalInSeconds(3)
    			.RepeatForever())
    		.Build();
    
  2. CronTrigger基于日历的概念进行作业启动计划。

    	ITrigger trigger1 = TriggerBuilder.Create()
    		.WithIdentity("myTrigger1", "group1")
    		.ForJob(job)
    		.WithCronSchedule("0 0/3 * * ?")
            .Build();
    

    CronExpression表达式 :https://www.cnblogs.com/yaowen/p/3779284.html

  3. 优先级(priority) 方法:.WithPriority(5)

如果你的trigger很多(或者Quartz线程池的工作线程太少),Quartz可能没有足够的资源同时触发所有的trigger;这种情况下,你可能希望控制哪些trigger优先使用Quartz的工作线程,要达到该目的,可以在trigger上设置priority属性。

比如,你有N个trigger需要同时触发,但只有Z个工作线程,优先级最高的Z个trigger会被首先触发。如果没有为trigger设置优先级,trigger使用默认优先级,值为5;priority属性的值可以是任意整数,正数、负数都可以。(只有同时触发的trigger之间才会比较优先级。10:59触发的trigger总是在11:00触发的trigger之前执行。)

Job与Trigger

Trigger对于job而言就好比一个驱动器;没有触发器来定时驱动作业,作业就无法运行;

对于Job而言,一个job可以对应多个Trigger,但对于Trigger而言,一个Trigger只能对应一个job;所以一个Trigger 只能被指派给一个 Job;如果你需要一个更复杂的触发计划,你可以创建多个 Trigger 并指派它们给同一个 Job。(Trigger实例对应一个JobDetail实例,Job类可以添加到多个JobDetail实例中)

Scheduler 是基于配置在 Job上的 Trigger 来决定正确的执行计划的。

JobDataMap

重点介绍一下JobDataMap,这是一个非常好用且被我们忽视的属性。

JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据;JobDataMap是 IDictionary<TKey, TValue> 接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。

JobDetail 和 Trigger 示例都可以设置 JobDataMap(通过UsingJobData()方法)

	IJobDetail job = JobBuilder.Create<HelloJob>()
		.WithIdentity("myJob", "group1")
		.UsingJobData("jobDetail", "J")
		.Build();
	
	ITrigger trigger = TriggerBuilder.Create()
		.WithIdentity("myTrigger", "group")
		.UsingJobData("trigger", "T")
		.WithCronSchedule("0/3 * * * * ?")
		.Build();
	await scheduler.ScheduleJob(job, trigger);

传递的值可以通过IJob.Execute(IJobExecutionContext context) 中context.MergedJobDataMap获取

    public class HelloJob : IJob
    {
        public Task Execute(IJobExecutionContext context)
        {
            var jobDataMap = context.MergedJobDataMap;
            Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-jobDetail:{jobDataMap["jobDetail"]}-trigger:{jobDataMap["trigger"]}");
            return Task.CompletedTask;
        }
    }

执行结果:

11:36:36-jobDetail:J-trigger:T
11:36:39-jobDetail:J-trigger:T
11:36:42-jobDetail:J-trigger:T
...

需要注意的是:

  1. MergedJobDataMap是将JobDetail.JobDataMap和Trigger.JobDataMap的值合并的,如果key重复,将读取Trigger中相同可以的值。
  2. 可以通过 context.JobDetail.JobDataMap 和 context.Trigger.JobDataMap分别读取。
  3. 在IJob.Execute()方法中修改任何JobDataMap值,是不会影响到下次Job执行JobDataMap的值的。只在本次Job中有效。

那有什么办法让本次执行的状态修改,影响到以一次执行呢?即修改JobDataMap的值,下一次执行取出的是上一次修改过的?办法是有的,给Job类打标签

Job属性标签

PersistJobDataAfterExecution:将该注解加在job类上,告诉Quartz在成功执行了job类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。

    [PersistJobDataAfterExecution]
    public class HelloJob : IJob
    {
        public Task Execute(IJobExecutionContext context)
        {
            var count = context.JobDetail.JobDataMap.GetInt("Count");
            Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-Count:{count}");

            count++;
            context.JobDetail.JobDataMap.Put("Count", count);

            return Task.CompletedTask;
        }
    }                                       
	StdSchedulerFactory factory = new StdSchedulerFactory();
	var scheduler = await factory.GetScheduler();
	await scheduler.Start();
	
	IJobDetail job1 = JobBuilder.Create<HelloJob>()
		.WithIdentity("myJob.1", "group")
		.UsingJobData("Count", "1")
		.Build();
	
	ITrigger trigger1 = TriggerBuilder.Create()
		.WithIdentity("myTrigger.1", "group")
		.WithCronSchedule("0/3 * * * * ?")
		.Build();
					
	await scheduler.ScheduleJob(job1, trigger1);  

执行结果:

14:17:09-Count:1
14:17:12-Count:2
14:17:15-Count:3
...

如果使用了[PersistJobDataAfterExecution]标签,将强烈建议同时使用[DisallowConcurrentExecution]标签,因为当同一个job(JobDetail)的两个实例被并发执行时,由于竞争,JobDataMap中存储的数据很可能是不确定的。

DisallowConcurrentExecution:将该注解加到job类上, 告诉Quartz不要并发地执行同一个job定义的多个实例 。

举例说明就是:将 HelloJob 添加到 job1(IJobDetail )

	IJobDetail job1 = JobBuilder.Create<HelloJob>()
		.WithIdentity("myJob.1", "group")
		.UsingJobData("flag", "myJob.1")
		.Build();

job1绑定触发器trigger1(ITrigger) 每秒执行一次

	ITrigger trigger1 = TriggerBuilder.Create()
		.WithIdentity("myTrigger.1", "group")
		.WithCronSchedule("0/1 * * * * ?")
		.Build();
	await scheduler.ScheduleJob(job1, trigger1);

HelloJob 的执行耗时为2秒

    public class HelloJob : IJob
    {
        public Task Execute(IJobExecutionContext context)
        {
            var flag = context.MergedJobDataMap.GetString("flag");
            //模拟耗时2秒
            Thread.Sleep(2000);
            Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-flag:{flag}");

            return Task.CompletedTask;
        }
    }

首先不打 DisallowConcurrentExecution 标签,看看输出结果:

任务启动:14:39:05
14:39:07-flag:myJob.1
14:39:08-flag:myJob.1
14:39:09-flag:myJob.1
14:39:10-flag:myJob.1
...

通过结果输入,可以看到,第一次任务是从14:39:05开始,14:39:07结束;第二次的任务接时间是14:39:08,退出开始时间是14:39:06,以此类推...也就说前一个任务未完成,并不影响之后任务的开始

接着我们个 HelloJob 打上 DisallowConcurrentExecution 属性标签

    [DisallowConcurrentExecution]
    public class HelloJob : IJob
    {
        public Task Execute(IJobExecutionContext context)
        {
            var flag = context.MergedJobDataMap.GetString("flag");
            //模拟耗时2秒
            Thread.Sleep(2000);
            Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-flag:{flag}");

            return Task.CompletedTask;
        }
    }

看看输出结果:

任务启动:14:47:34
14:47:36-flag:myJob.1
14:47:38-flag:myJob.1
14:47:40-flag:myJob.1
14:47:42-flag:myJob.1
...

通过结果输出可以看到,文本输出是每两秒一次,也就说,前一个任务未完成,之后任务不会开始。即不会创建一个新的 HelloJob 实例。这也就不会并发处理任务了。

需要注意的是,DisallowConcurrentExecution 属性标签,限制的是 JobDetail ,而不是 Job(HelloJob)。同一个JobDetail 实例创建的 Job 不会并发。但,不同的 JobDetail 实例创建的 Job 是可以并发的。

我们再创建一组关于 HelloJob 的任务:job2(IJobDetail),trigger2(ITrigger),HelloJob 不变。

    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine($"任务启动:{DateTime.Now.ToLongTimeString()}");
            StdSchedulerFactory factory = new StdSchedulerFactory();
            var scheduler = await factory.GetScheduler();
            await scheduler.Start();
            IJobDetail job1 = JobBuilder.Create<HelloJob>()
                .WithIdentity("myJob.1", "group")
                .UsingJobData("flag", "myJob.1")
                .Build();
            ITrigger trigger1 = TriggerBuilder.Create()
                .WithIdentity("myTrigger.1", "group")
                .WithCronSchedule("0/1 * * * * ?")
                .Build();
            
            IJobDetail job2 = JobBuilder.Create<HelloJob>()
                .WithIdentity("myJob.2", "group")
                .UsingJobData("flag", "myJob.2")
                .Build();
            ITrigger trigger2 = TriggerBuilder.Create()
                .WithIdentity("myTrigger.2", "group")
                .WithCronSchedule("0/1 * * * * ?")
                .Build();

            await scheduler.ScheduleJob(job1, trigger1);
            await scheduler.ScheduleJob(job2, trigger2);

            Console.ReadKey();
        }
    }

看看输出结果:

任务启动:15:02:24
15:02:26-flag:myJob.1
15:02:26-flag:myJob.2
15:02:28-flag:myJob.1
15:02:28-flag:myJob.2
15:02:30-flag:myJob.1
15:02:30-flag:myJob.2
...

通过结果输出可以看到,同一个JobDetail,是没有每秒执行的,即前一个任务没有完成,后面的任务不会执行。但不同的JobDetail,却在同一时间执行了。

Job的配置

像上面示例中,我们配置Job,基本都是硬编码,我们可以把配置移到配置文件中,方便修改和添加

默认配置文件名:quartz_jobs.xml

<?xml version="1.0" encoding="UTF-8"?>

<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">
  <processing-directives>
    <overwrite-existing-data>true</overwrite-existing-data>
  </processing-directives>

  <schedule>
    <job>
      <name>myJob.1</name>
      <group>group</group>
      <description>Hello World!</description>
      <job-type>Sayook.Schedule.Client.HelloJob, Sayook.Schedule.Client</job-type>
      <job-data-map>
        <entry>
          <key>flag</key>
          <value>myJob.1</value>
        </entry>
      </job-data-map>
    </job>
    <trigger>
      <cron>
        <name>myTrigger.1</name>
        <group>group</group>
        <description>Hello World! </description>
        <job-name>myJob.1</job-name>
        <job-group>group</job-group>
        <job-data-map>
          <entry>
            <key>key</key>
            <value>1</value>
          </entry>
        </job-data-map>
        <cron-expression>0/3 * * * * ?</cron-expression>
      </cron>
    </trigger>
  </schedule>
</job-scheduling-data>

我们可以将 job_scheduling_data_2_0.xsd 文件添加到 VisualStudio2019 的XML架构(直接在IDE顶部搜索框搜索xml架构),我们在编写xml配置文件的时候就会有提示和验证了。

    class Program
    {
        static async Task Main(string[] args)
        {
            var properties = new NameValueCollection
            {
                ["quartz.plugin.xml.type"] = "Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz.Plugins",
                ["quartz.plugin.xml.fileNames"] = "quartz_jobs.xml",
                // this is the default
                ["quartz.plugin.xml.FailOnFileNotFound"] = "true",
                // this is not the default
                ["quartz.plugin.xml.failOnSchedulingError"] = "true"
            };

            StdSchedulerFactory factory = new StdSchedulerFactory(properties);
            var scheduler = await factory.GetScheduler();
            await scheduler.Start();
            Console.ReadKey();
        }
    }

["quartz.plugin.xml.FailOnFileNotFound"] = "true",
["quartz.plugin.xml.failOnSchedulingError"] = "true"

上面两个配置文件强烈建议添加,以为这样,配置文件错误了,会有详细的异常信息抛出,以便修改,负责是不会报错,很难定位问题。

使用配置文件,要引用包:Quartz.Plugins

相关配置可查看文章:Quartz.NET 配置文件详解 https://www.cnblogs.com/abeam/p/8044460.html

应用示例

基于.NetCore的依赖注入,对Quartz.Net的使用

示例里面包含:

  1. 可视化面板控制的调度应用[Sayook.Schedule.Manager]
  2. 使用控制台应用程序创建的 泛型主机 调用应用[Sayook.Schedule.Client]

通过xml文件配置Job,后续维护、新增Job,对代码的改动都非常小, 是最轻量级的使用。

示例代码地址: https://gitee.com/sayook/Sayook.Schedule.Framework

posted @ 2020-05-25 16:07  稻草堆上打着滚儿  阅读(397)  评论(0编辑  收藏  举报