net core WebApi——定時任務Quartz

  • 2019 年 10 月 3 日
  • 筆記

前言

本來打算昨天都開始寫這篇,就因為要把小團隊的部落格整理匯總,一看二哈的部落格那麼多,一個個複製粘貼肯定麻煩(其實是我自己覺得複製麻煩),所以穿插著寫了個小爬蟲,後續寫差不多了就拿出來晾晾吧(py菜雞水平)。

之前開發的時候,忽略了記錄,等到想寫點兒啥跟後台有關的東西的時候,還得一點點回憶,最近是因為同事給我說,"哎,每個月把數據給我統計下做個介面展示啊"。一想到每個月我要做次操作就頭疼,咦,不對,這不就是寫個定時任務就搞定了嘛。

Quartz

其實在選這個定時器的類庫的時候,我在Hangfire兩者間徘徊,後來是想到不管用什麼方法什麼工具都是次要的,主要看你怎麼用,用到哪,圖形介面是需要但不是必要,分秒級別的控制也都是看你自己業務需要,定時器就後台掛起運行就行了沒必要讓我看見,想操作了再說吧,就這樣愉快的決定使用Quartz。

首先,依然是在我們Util的工程引入包。

引入完成後,在我們的入口Startup中添加實例的註冊聲明。

        public IServiceProvider ConfigureServices(IServiceCollection services)          {              ServiceInjection.ConfigureRepository(services);                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);              //任務調度              services.TryAddSingleton<ISchedulerFactory, StdSchedulerFactory>();          }

SchedulerFactory任務調度就好比一個公司的老大,Trigger就是一個項管,Job就是苦逼的碼農,老大想要一天搞個app,就跟項管說一句,我要一天後要東西,這時候項管心裡就有數了,一天後的那個時間,找到碼農,直接剝奪他的程式碼執行,好了app出來了,苦逼的結束並不意味著真的結束,這老大一看可以啊,好了,以後每天我要一個成品app,如此循環往複,項管不厭其煩,碼農換了又換(當然job不會)。

項管還會有多個,每個項管下面可不止一個碼農。

像這樣的情況可能有些誇張,但是類似的情況卻真實存在。

ok,完了之後,我們來創建一個MyJob。

    public class MyJob : IJob      {          public Task Execute(IJobExecutionContext context)          {              return Task.Run(() =>              {                  LogUtil.Debug("執行MyJob");              });          }      }

之後我們來寫個簡單的QuartzUtil。

    public class QuartzUtil      {          private static ISchedulerFactory _schedulerFactory;          private static IScheduler _scheduler;            /// <summary>          /// 添加任務          /// </summary>          /// <param name="type">類</param>          /// <param name="jobKey">鍵</param>          /// <param name="trigger">觸發器</param>          public static async Task Add(Type type, JobKey jobKey, ITrigger trigger = null)          {              Init();              _scheduler = await _schedulerFactory.GetScheduler();                await _scheduler.Start();                if (trigger == null)              {                  trigger = TriggerBuilder.Create()                      .WithIdentity("april.trigger")                      .WithDescription("default")                      .WithSimpleSchedule(x=>x.WithMisfireHandlingInstructionFireNow().WithRepeatCount(-1))                      .Build();              }              var job = JobBuilder.Create(type)                  .WithIdentity(jobKey)                  .Build();                await _scheduler.ScheduleJob(job, trigger);          }          /// <summary>          /// 恢復任務          /// </summary>          /// <param name="jobKey">鍵</param>          public static async Task Resume(JobKey jobKey)          {              Init();              _scheduler = await _schedulerFactory.GetScheduler();              LogUtil.Debug($"恢復任務{jobKey.Group},{jobKey.Name}");              await _scheduler.ResumeJob(jobKey);          }          /// <summary>          /// 停止任務          /// </summary>          /// <param name="jobKey">鍵</param>          public static async Task Stop(JobKey jobKey)          {              Init();              _scheduler = await _schedulerFactory.GetScheduler();              LogUtil.Debug($"暫停任務{jobKey.Group},{jobKey.Name}");              await _scheduler.PauseJob(jobKey);          }          /// <summary>          /// 初始化          /// </summary>          private static void Init()          {              if (_schedulerFactory == null)              {                  _schedulerFactory = AprilConfig.ServiceProvider.GetService<ISchedulerFactory>();              }          }      }

感謝jiulang指點,非同步編程避免使用async void,因為這樣會導致異常無法捕獲。

觸發器的使用,有很多種方式,可以使用簡單的執行一次/多久執行一次/循環執行幾次等等。

還有可以使用Cron表達式:

簡單來說,corn從左到右(用空格隔開):秒 分 小時 月份中的日期 月份 星期中的日期 年份,舉個例子,就像開頭說的,讓我每隔一個月執行一次統計,寫法就是 0 0 0 1 * ?,當然這就有涉及到什麼符號的問題了,這種不需要強記,需要的時候查下就行,推薦一個工具站吧,Cron校驗工具。

測試

感覺我的部落格內容好單調,內容框架就是開頭,程式碼,測試,結尾,唉

不過做啥東西,測試少不了,最起碼你的東西能用,才說明可行。

我們在Values添加一個方法,這裡我們5s一執行(懶得等)。

        [HttpGet]          [Route("QuartzTest")]          public void QuartzTest(int type)          {              JobKey jobKey = new JobKey("demo","group1");              switch (type)              {                  //添加任務                  case 1:                      var trigger = TriggerBuilder.Create()                              .WithDescription("觸發器描述")                              .WithIdentity("test")                              //.WithSchedule(CronScheduleBuilder.CronSchedule("0 0/30 * * * ? *").WithMisfireHandlingInstructionDoNothing())                              .WithSimpleSchedule(x=>x.WithIntervalInSeconds(5).RepeatForever().WithMisfireHandlingInstructionIgnoreMisfires())                              .Build();                      QuartzUtil.Add(typeof(MyJob), jobKey, trigger);                      break;                  //暫停任務                  case 2:                      QuartzUtil.Stop(jobKey);                      break;                  //恢復任務                  case 3:                      QuartzUtil.Resume(jobKey);                      break;              }          }

讓我們來愉快的運行吧,記得appsettings配置個路徑訪問白名單。

测试

一番1,2,3輸入完之後,我們來看下日誌。
测试

  • 執行任務--- ok
  • 暫停任務--- ok
  • 恢復任務--- ok

問題及解決方法

但是問題出現了,暫停恢復後,連執行了多次(具體看你間隔時間以及你的頻率),這個是有點兒怪異,當時我記得這個問題讓我鼓搗了好半天,也是各種查資料查方法,但實際呢這個是Quartz的保護機制,為了防止你的操作是因為不可預知的問題導致的,所以有個重做錯過的任務,另外我們的程式碼中觸發器也有這個配置WithMisfireHandlingInstructionIgnoreMisfires。

我們來去掉這個重做機制並測試。

测试

CronTrigger

規則 介紹
withMisfireHandlingInstructionDoNothing 不觸發立即執行; 等待下次Cron觸發頻率到達時刻開始按照Cron頻率依次執行
withMisfireHandlingInstructionIgnoreMisfires 以錯過的第一 個頻率時間立刻開始執行; 重做錯過的所有頻率周期後; 當下一次觸發頻率發生時間大於當前時間後,再按照正常的Cron頻率依次執行
withMisfireHandlingInstructionFireAndProceed 以當前時間為觸發頻率立刻觸發一次執行; 然後按照Cron頻率依次執行

SimpleTrigger

規則 介紹
withMisfireHandlingInstructionFireNow 以當前時間為觸發頻率立即觸發執行; 執行至FinalTIme的剩餘周期次數;以調度或恢復調度的時刻為基準的周期頻率,FinalTime根據剩餘次數和當前時間計算得到; 調整後的FinalTime會略大於根據starttime計算的到的FinalTime值
withMisfireHandlingInstructionIgnoreMisfires 以錯過的第一個頻率時間立刻開始執行; 重做錯過的所有頻率周期;當下一次觸發頻率發生時間大於當前時間以後,按照Interval的依次執行剩下的頻率; 共執行RepeatCount+1次
withMisfireHandlingInstructionNextWithExistingCount 不觸發立即執行; 等待下次觸發頻率周期時刻,執行至FinalTime的剩餘周期次數; 以startTime為基準計算周期頻率,並得到FinalTime; 即使中間出現pause,resume以後保持FinalTime時間不變
withMisfireHandlingInstructionNowWithExistingCount 以當前時間為觸發頻率立即觸發執行; 執行至FinalTIme的剩餘周期次數; 以調度或恢復調度的時刻為基準的周期頻率,FinalTime根據剩餘次數和當前時間計算得到; 調整後的FinalTime會略大於根據starttime計算的到的FinalTime值
withMisfireHandlingInstructionNextWithRemainingCount 不觸發立即執行; 等待下次觸發頻率周期時刻,執行至FinalTime的剩餘周期次數; 以startTime為基準計算周期頻率,並得到FinalTime; 即使中間出現pause,resume以後保持FinalTime時間不變
withMisfireHandlingInstructionNowWithRemainingCount 以當前時間為觸發頻率立即觸發執行; 執行至FinalTIme的剩餘周期次數; 以調度或恢復調度的時刻為基準的周期頻率,FinalTime根據剩餘次數和當前時間計算得到; 調整後的FinalTime會略大於根據starttime計算的到的FinalTime值

配置規則介紹參考:https://blog.csdn.net/yangshangwei/article/details/78539433

之前在net framework遇到過一個問題,IIS回收問題,網站在20分鐘無請求後就停了,任務也緊跟著停了,當時的解決方法是做個windows服務來定時請求網站保持活躍,當然也可以通過禁止回收來保持網站一直運行。

net core中還沒部署運行,如果有相關問題,後續也會補充上來一起交流解決。

小結

定時任務在一個後台系統中一般使用場景還算廣泛,主要是sql數據統計,sql/文件備份,定時推送等,具體問題具體分析,net core 3.0都已經問世了,學無止境啊。

Tags: