Quartz.net 3.x使用總結(一)——入門介紹

撈月亮的猴子發表於2019-01-16

1.Quartz.net簡介

  Quartz.NET是一個強大、開源、輕量級的任務排程框架。任務排程在我們的開發中經常遇到,如說:每天晚上三點讓程式或網站執行某些程式碼,或者每隔5秒種執行一個方法等。Windows計劃任務也能實現類似的任務排程,但是Quartz.net有一些有優秀的特性,如:資料庫支援,叢集,外掛,支援cron-like表示式等等。  

官網:http://www.quartz-scheduler.net/

原始碼:https://github.com/quartznet/quartznet

示例:https://www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html 

在使用Quart.net前,我們先理解下quartz的構成和基本工作流程,Quartz包含以下五個基本部分

Scheduler           排程器,quartz工作時的獨立容器
Trigger             觸發器,定義了排程任務的時間規則
Job                 排程的任務
ThreadPool          執行緒池(不是clr中的執行緒池),任務最終交給執行緒池中的執行緒執行
JobStore            RAWStore和DbStore兩種,job和trigger都存放在JobStore中

Quartz的基本工作流程:scheduler是quartz的獨立執行容器,trigger和job都可以註冊在scheduler容器中,其中trigger是觸發器,用於定義排程任務的時間規則,job是被排程的任務,一個job可以有多個觸發器,而一個觸發器只能屬於一個job。Quartz中一個排程執行緒QuartzSchedulerThread,排程執行緒可以找到將要被觸發的trigger,通過trigger找到要執行的job,然後在ThreadPool中獲取一個執行緒來執行這個job。JobStore主要作用是存放job和trigger的資訊。

2.簡單使用

用五分鐘看一個簡單的例子吧,這個例子中通過排程器工廠StdSchedulerFactory獲取一個排程器例項scheduler,然後定義Job和trigger,並註冊到排程器中,最後啟動scheduler就可以執行我們的任務了。程式碼如下:

    class Program
    {
        static void Main(string[] args)
        {
            //排程器,生成例項的時候執行緒已經開啟了,不過是在等待狀態
            StdSchedulerFactory factory = new StdSchedulerFactory();
            IScheduler scheduler = factory.GetScheduler().Result;

            //建立一個Job,繫結MyJob
            IJobDetail job = JobBuilder
                                .Create<MyJob>()                     //獲取JobBuilder
                                .WithIdentity("jobname1", "group1")  //新增Job的名字和分組
                                .WithDescription("一個簡單的任務")     //新增描述
                                .Build();                            //生成IJobDetail

            //建立一個觸發器
            ITrigger trigger =
                TriggerBuilder.Create()                                  //獲取TriggerBuilder
                              .StartAt(DateBuilder.TodayAt(01, 00, 00))  //開始時間,今天的1點(hh,mm,ss),可使用StartNow()
                              .ForJob(job)                               //將觸發器關聯給指定的job
                              .WithPriority(10)                          //優先順序,當觸發時間一樣時,優先順序大的觸發器先執行
                              .WithIdentity("tname1", "group1")          //新增名字和分組
                              .WithSimpleSchedule(x => x.WithIntervalInSeconds(1) //排程,一秒執行一次,執行三次
                                                        .WithRepeatCount(3)
                                                        .Build())
                              .Build();

            //start讓排程執行緒啟動【排程執行緒可以從jobstore中獲取快要執行的trigger,然後獲取trigger關聯的job,執行job】
            scheduler.Start();           
            //將job和trigger註冊到scheduler中
            scheduler.ScheduleJob(job, trigger).Wait();
            Console.ReadKey();
        }
    }


    #region MyJob
    /// <summary>
    /// 一個簡單的Job,所有的Job都要實現IJob介面
    /// </summary>
    public class MyJob : IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {
            await Task.Run(() =>
            {
                Console.WriteLine("hello quartz!");
                //JobDetail的key就是job的分組和job的名字
                Console.WriteLine($"JobDetail的組和名字:{context.JobDetail.Key}"); 
                Console.WriteLine();
            });
        }
    }
    #endregion

程式碼中的註釋已經很詳細了,這裡就不多介紹程式碼,我們可以看到通過Quartz來排程一個任務十分簡單,執行結果如下:

3.TriggerBuilder介紹

  在Quartz中Trigger的作用是定義Job何時執行。Quartz.net提供了四種觸發策略:SimpleSchedule,CalendarIntervalSchedule,DailyTimeIntervalSchedule和CronSchedule。TriggerBuilder顧名思義就是用來建立Trigger的。

3.1  SimpleSchedule

  Simpleschedule 是最簡單的一種觸發策略,它的作用類似於timer,可以設定間隔幾秒/幾分鐘/幾小時執行一次,如建立一秒執行一次的觸發器如下

          ITrigger trigger =
                   TriggerBuilder.Create()
                                 .StartNow()
                                 .WithIdentity("tname1", "group1")
                                 .WithSimpleSchedule(x => x.WithIntervalInSeconds(1)  //設定時間間隔,時分秒              
                                                           .WithRepeatCount(3))       //設定執行次數,總共執行3+1次,
                                 .Build();

3.2  CalendarIntervalSchedule

  CalendarIntervalSchedule擴充套件了Simplescheduler的功能,Simplescheduler只能在時分秒的維度上指定時間間隔,那麼就有一個問題,如果我們想一個月執行一次怎麼辦呢?要知道每個月的天數是不一樣的,用SimpleSchedule實現起來就十分麻煩了。CalendarIntervalSchedule可以實現時分秒天周月年的維度上執行輪詢。如建立一個月執行一次的觸發器如下:這樣

            ITrigger trigger =
                    TriggerBuilder.Create()
                         .StartAt(DateTimeOffset.Parse("2018/1/6 13:17:00"))
                         .WithIdentity("tname1", "group1")
                         .WithCalendarIntervalSchedule(x => x.WithIntervalInMonths(1))  //一月執行一次
                         .Build();

3.3  DailyTimeIntervalSchedule

  DailyTimeIntervalSchedule主要用於指定每週的某幾天執行,如我們想讓每週的週六週日的8:00-20:00,每兩秒執行一次,建立觸發器如下:

            ITrigger trigger =
               TriggerBuilder.Create()
                      .StartAt(DateTimeOffset.Parse("2018/1/6 13:17:00"))
                      .WithIdentity("tname1", "group1")
                      .WithDailyTimeIntervalSchedule(x => x.OnDaysOfTheWeek(new DayOfWeek[] { DayOfWeek.Saturday, DayOfWeek.Sunday }) //週六和週日
                                                     .StartingDailyAt(TimeOfDay.HourMinuteAndSecondOfDay(8, 00, 00)) //8點開始
                                                     .EndingDailyAt(TimeOfDay.HourMinuteAndSecondOfDay(20, 00, 00))  //20點結束
                                                     .WithIntervalInSeconds(2)                                       //兩秒執行一次,可設定時分秒維度
                                                     .WithRepeatCount(3))                                            //一共執行3+1次
                      .Build();

3.4  CronSchedule

  CronSchedule是應用最多的觸發策略,通過Cron表達是我們可以輕鬆地表示任意的時間節點,下邊的程式碼建立了一個每隔5秒執行一次的觸發器

              ITrigger trigger =
                          TriggerBuilder.Create()
                               .StartAt(DateTimeOffset.Parse("2018/1/6 13:17:00"))
                               .WithIdentity("tname1", "group1")
                               .WithCronSchedule("3/5 * * * * ?")  //五秒執行一次
                               .Build();

Cron表示式

cron表示式有七個部分組成,以此是秒、分、時、天、月、周、年,其中年是可選的。

位置時間域允許值特殊值
1 0-59 , - * /
2 分鐘 0-59 , - * /
3 小時 0-23 , - * /
4 日期 1-31 , - * ? / L W C
5 月份 1-12 , - * /
6 星期 1-7 , - * ? / L C #
7 年份(可選) 1-31 , - * /


星號(*):可用在所有欄位中,表示對應時間域的每一個時刻,例如, 在分鐘欄位時,表示“每分鐘”;

問號(?):該字元只在日期和星期欄位中使用,它通常指定為“無意義的值”,相當於點位符;

減號(-):表達一個範圍,如在小時欄位中使用“10-12”,則表示從10到12點,即10,11,12;

逗號(,):表達一個列表值,如在星期欄位中使用“MON,WED,FRI”,則表示星期一,星期三和星期五;

斜槓(/):x/y表達一個等步長序列,x為起始值,y為增量步長值。如5/15在分鐘欄位中表示5,20,35,50;

L:該字元只在日期和星期欄位中使用,代表“Last”的意思,但它在兩個欄位中意思不同。L在日期欄位中,表示這個月份的最後一天,如一月的31號,非閏年二月的28號;如果L用在星期中,則表示星期六,等同於7。但是,如果L出現在星期欄位裡,而且在前面有一個數值X,則表示“這個月的最後X天”,例如,6L表示該月的最後星期五;

W:該字元只能出現在日期欄位裡,是對前導日期的修飾,表示離該日期最近的工作日。例如15W表示離該月15號最近的工作日,如果該月15號是星期六,則匹配14號星期五;如果15日是星期日,則匹配16號星期一;如果15號是星期二,那結果就是15號星期二。但必須注意關聯的匹配日期不能夠跨月,如你指定1W,如果1號是星期六,結果匹配的是3號星期一,而非上個月最後的那天。W字串只能指定單一日期,而不能指定日期範圍;

LW組合:在日期欄位可以組合使用LW,它的意思是當月的最後一個工作日;

井號(#):該字元只能在星期欄位中使用,表示當月某個工作日。如6#3表示當月的第三個星期五(6表示星期五,#3表示當前的第三個),而4#5表示當月的第五個星期三,假設當月沒有第五個星期三,忽略不觸發;

Cron表示式對特殊字元的大小寫不敏感,對代表星期的縮寫英文大小寫也不敏感。

一些例子:

表示式說明
0 0 12 * * ? 每天12點執行
0 15 10 ? * * 每天10:15執行
0 15 10 * * ? 每天10:15執行
0 15 10 * * ? * 每天10:15執行
0 15 10 * * ? 2008 在2008年的每天10:15執行
0 * 14 * * ? 每天14點到15點之間每分鐘執行一次,開始於14:00,結束於14:59。
0 0/5 14 * * ? 每天14點到15點每5分鐘執行一次,開始於14:00,結束於14:55。
0 0/5 14,18 * * ? 每天14點到15點每5分鐘執行一次,此外每天18點到19點每5鍾也執行一次。
0 0-5 14 * * ? 每天14:00點到14:05,每分鐘執行一次。
0 10,44 14 ? 3 WED 3月每週三的14:10分和14:44執行。
0 15 10 ? * MON-FRI 每週一,二,三,四,五的10:15分執行。
0 15 10 15 * ? 每月15日10:15分執行。
0 15 10 L * ? 每月最後一天10:15分執行。
0 15 10 ? * 6L 每月最後一個星期五10:15分執行。
0 15 10 ? * 6L 2007-2009 在2007,2008,2009年每個月的最後一個星期五的10:15分執行。
0 15 10 ? * 6#3 每月第三個星期五的10:15分執行。

4.Scheduler介紹

  排程器scheduler是Quartz中的獨立工作容器,所有的Trigger和Job都需要註冊到scheduler中才能工作。我們可以通過SchedulerFactory來獲取scheduler例項。如下:

//1.獲取預設的標準Scheduler引用
IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler().Result;


//2.通過程式碼配置scheduler
 NameValueCollection properties = new NameValueCollection
            {
                //scheduler的名字
                ["quartz.scheduler.instanceName"] = "MyScheduler",
                // 設定執行緒池中執行緒個數為20個
                ["quartz.threadPool.threadCount"] = "20",
                ["quartz.threadPool.type"] = "Quartz.Simpl.SimpleThreadPool, Quartz",
                //JobStore型別為記憶體儲存
                ["quartz.jobStore.type"] = "Quartz.Simpl.RAMJobStore, Quartz"
            };
            ISchedulerFactory factroy = new StdSchedulerFactory(properties);
            IScheduler scheduler= await factroy .GetScheduler();

這裡列一些會經常用的到方法,方法比較簡單直觀,就不一一展示了。

          scheduler.PauseJob(JobKey.Create("jobname", "groupname"));//暫停job
          scheduler.ResumeJob(JobKey.Create("jobname", "groupname"));//重新啟動job
          scheduler.DeleteJob(JobKey.Create("jobname", "groupname"));//刪除job
          scheduler.PauseTrigger(new TriggerKey("triggername", "groupname"));//暫停trigger
          scheduler.ResumeTrigger(new TriggerKey("triggername", "groupname"));//重新啟動trigger
          scheduler.UnscheduleJob(new TriggerKey("triggername", "groupname"));//刪除trigger
          scheduler.GetTriggersOfJob(JobKey.Create("jobname", "groupname"));//獲取一個job的所有key    
          scheduler.Standby();  //暫停所有的觸發器,可通過shceduler.Start()重啟
          scheduler.Shutdown(); //關閉scheduler,釋放資源。通過Shutdown()關閉後,不能通過Start()重啟
          scheduler.GetMetaData();//獲取scheduler的後設資料
          scheduler.Clear();//清空容器中所有的IJob,ITrigger

         //排程多個任務
         Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>> triggersAndJobs = new Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>>();
         triggersAndJobs.Add(job1, new List<ITrigger>() { trigger1,trigger2});
         triggersAndJobs.Add(job2, new List<ITrigger>() { trigger3});
         await scheduler.ScheduleJobs(triggersAndJobs, true);

 

5.Calendar介紹

  通過上邊的介紹,我們知道通過觸發器Trigger可以設定Job的執行時間,但是有時候只使用Trigger來排程比較麻煩,如一年中每天都執行,但是元旦、聖誕節和情人節這三天不執行。使用trigger也可以實現,但是比較麻煩,如果我們有一種方法可以方便地排除這三天就好辦了,Calendar主要作用就是為了排除Trigger中一些特定的時間節點。看一個簡單的栗子:

    class Program
    {
        static void Main(string[] args)
        {
            //排程器
            IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler().Result;
            //JobDetail
            IJobDetail job = JobBuilder
                                .Create<MyJob>()
                                .Build();

            //獲取一個Calendar例項
            DailyCalendar calendar = new DailyCalendar(DateBuilder.DateOf(19, 0, 0).DateTime, DateBuilder.DateOf(23, 0, 0).DateTime);//21~23點不執行
            //將Calendar註冊到Scheduler中
            scheduler.AddCalendar("myCalendar", calendar, true, true);//引數依次是:calendarname,calendar,是否替換同名clendar,是否更新trigger
            //獲取一個觸發器,並將calendar繫結到觸發器上
            ITrigger trigger =
                          TriggerBuilder.Create()
                               .StartNow()
                               .WithIdentity("tname1", "group1")
                               .WithCronSchedule("0/2 * * * * ?")  //2秒執行一次
                               .ModifiedByCalendar("myCalendar")   //把Calendar繫結到trigger
                               .Build();

            //start讓排程執行緒啟動
            scheduler.Start();
            //將job新增到排程器中,同時將trigger繫結到job
            scheduler.ScheduleJob(job, trigger).Wait();
            Console.ReadKey();
        }
    }

    #region MyJob,繼承IJob介面
    /// <summary>
    /// 一個簡單的Job
    /// </summary>
    public class MyJob : IJob
    {public Task Execute(IJobExecutionContext context)
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"觸發時間:{context.ScheduledFireTimeUtc?.LocalDateTime},下次觸發時間:{context.NextFireTimeUtc?.LocalDateTime}");
            });
        }

    }
    #endregion

執行結果:

使用Calendar的流程是:首先獲取一個Calendar例項,然後將Calendar註冊到scheduler容器中,在將Calendar繫結到觸發器上即可。Quartz.net中一共提供了六種Calendar,六種Calendar的用法大同小異,列舉如下:

            ////【1】.DailyCalendar 用於排除一天中的某一段時間
            DailyCalendar calendar = new DailyCalendar(DateBuilder.DateOf(19, 0, 0).DateTime, DateBuilder.DateOf(23, 0, 0).DateTime);//21~23點不執行


            ////【2】.WeeklyCalendar 用於排除一週中的某幾天
            WeeklyCalendar calendar = new WeeklyCalendar();
            calendar.SetDayExcluded(DayOfWeek.Sunday, true);//週日不執行 
            //注:如果想讓週日恢復執行,執行程式碼:  calendar.SetDayExcluded(DayOfWeek.Sunday, false);

            ////【3】.HolidayCalendar 用於排除某些日期
            HolidayCalendar calendar = new HolidayCalendar();
            calendar.AddExcludedDate(DateTime.Parse("2018/1/2")); //2018年1月2號不執行
            //注:如果想讓2019/1/9恢復執行,執行程式碼:  calendar.RemoveExcludedDate(DateTime.Parse("2018/1/2"));

            ////【4】.MonthlyCalendar 用於排除每個月的某天*************************************
            MonthlyCalendar calendar = new MonthlyCalendar();
            calendar.SetDayExcluded(8,true); //每個月的8號不執行
            //注:如果想讓8號恢復執行,執行程式碼:  calendar.SetDayExcluded(8, false);

            ////【5】AnnualCalendar 用於排除一年中的某些天*************************************
            AnnualCalendar calendar = new AnnualCalendar();
            calendar.SetDayExcluded(DateTime.Parse("2018/1/2"), true);//每年1月2號不執行
            //注:如果想讓1月8號恢復執行,執行程式碼:   calendar.SetDayExcluded(DateTime.Parse("2018/1/2"),true);

            ////【6】.CronCalendar 用於排除cron表示式表示的時間***************************
            CronCalendar calendar = new CronCalendar("* * * 2 1 ?"); //每年的1月2號不執行

6.Listener介紹

  TriggerListener和JobListener用法類似,這裡以JobListener為例來介紹Quartz中的Listener。JobListener用於在Job執行前、後和被拒絕時執行一些動作,和Asp.net中的filter很相似,用法並不複雜,看一個簡單的栗子:

    class Program
    {
        static void Main(string[] args)
        {
            //獲取排程器
            NameValueCollection pairs = new NameValueCollection() { { "quartz.threadPool.ThreadCount", "30" } };
            StdSchedulerFactory factory = new StdSchedulerFactory(pairs);
            IScheduler scheduler = factory.GetScheduler().Result; 

            //建立Job
            IJobDetail job = JobBuilder
                                .Create<MyJob>()                     //獲取JobBuilder
                                .WithIdentity("jobname1", "group1")  //新增Job的名字和分組
                                .Build();                            //生成IJobDetail


            //建立rigger
            ITrigger trigger =
                TriggerBuilder.Create()                                  //獲取JobBuilder
                              .StartAt(DateBuilder.TodayAt(01, 00, 00))  //開始時間,今天的1點(hh,mm,ss),可使用StartNow()
                              .WithPriority(10)                          //優先順序,當觸發時間一樣時,優先順序大的觸發器先執行
                              .WithIdentity("tname1", "group1")          //新增名字和分組
                              .WithSimpleSchedule(x => x.WithIntervalInSeconds(1)
                                                        .RepeatForever()
                                                        .Build())
                              .Build();

            //啟動排程器
            scheduler.Start();
           
            //myJobListener監控所有的job
            scheduler.ListenerManager.AddJobListener(new MyJobListener(), GroupMatcher<JobKey>.AnyGroup());
            //將job新增到排程器中,同時將trigger繫結到job
            scheduler.ScheduleJob(job, trigger).Wait();
            Console.ReadKey();
        }
    }


    #region MyJob,繼承IJob介面
    /// <summary>
    /// 一個簡單的Job
    /// </summary>
    public class MyJob : IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {
            await Task.Run(() =>
            {
                Console.WriteLine("hello quartz!");
                Console.WriteLine($"ThreadPool中的執行緒個數:{context.Scheduler.GetMetaData().Result.ThreadPoolSize}");
}); } } #endregion #region myJobListener,繼承IJobListener介面 /// <summary> /// 一個簡單的JobListener,triggerListener類似 /// </summary> public class MyJobListener : IJobListener { public string Name => "hello joblisener"; //job被拒絕時執行 public async Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default(CancellationToken)) { await Task.Run(() => { }); } //job開始前執行 public async Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default(CancellationToken)) { await Task.Run(() => { Console.WriteLine("myjob-------------begin"); }); } //job完成後執行 public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken = default(CancellationToken)) { await Task.Run(() => { Console.WriteLine("myjob---------------end");
          Console.WriteLine(); }); }
#endregion }

上邊例子中,Listener執行的動作很簡單,在Job執行前列印begin,執行後列印end,在實際開發中,我們可以在通過Listenter來記錄job的執行日誌,執行結果如下:

 

 這一篇主要介紹了Quartz中的一些基本概念和簡單用法,個人感覺Quartz3.x和2.x最大的優勢在於:支援非同步執行Job,所以建議將Job的Excute方法設計為非同步方法,這樣做可以提高任務排程和執行效率。

相關文章