有段日子沒有更新,寫點東西冒個泡 。這篇文章過來說個小東西,也是你們在平常開發中也常常須要面臨的問題:後臺定時任務處理。估計你們看到這句就已經聯想到 QuartZ 等相似第三方類庫了,很差意思,後邊的事情和它們沒有關係。這裏要展開的是用.Net Core 下的 Generic Host 配合封裝簡版定時任務處理框架的過程。至於什麼是Generic Host,簡單來講就是一個簡化版不含Http管道等的非Web應用託管宿主服務,至於它如何來,其內有着什麼樣的實現細節,官方介紹已經足夠。這篇文章主要仍是回到實際的基礎封裝過程實現層面,用一個小東西來演示如何在常見業務代碼中梳理職責,內容主要以下:數據庫
1. 概要分解windows
2. 封裝實現併發
3. 示例演示框架
4. 注意事項異步
一. 概要分解ide
若是對Generic Host 已經有了解的同窗可能也看過網上其餘文章,大多也都介紹用它如何實現定時任務處理。這些文章基本提供了一個通用實現,對業務實現仍是稍顯囉嗦。這兩天整理邏輯有個任務不得不臨時定時處理,想到這個東西,花了點時間處理了下,東西不復雜不過仍是想把這個思路分享給須要的朋友。函數
定時任務,分解來看特別簡單,就是兩個維度「 定時 + 任務 」,若是還有另一個維度,那就是 任務運行的託管服務。在託管平臺上添加定時規則,根據規則觸發任務,工做結束。ui
1. 關於定時,主要就是一套任務觸發的規則,其做爲一個調度者,只須要關心的是 在什麼時間,以何種頻率 觸發任務。 在.Net 下咱們經過定時器(Timer - 構造函數包含這兩個核心參數,.net 下有兩個Timer實現,一個是System.Timer.Timer,一個是System.Threading.Timer, 這裏用的第兩者,自由度更高)來實現,可是它不該該直接和具體的任務掛鉤,使用方也不該該每次都本身來處理Timer的初始化及相關回收釋放等相同操做,咱們須要的是使用方只需告知框架層要執行什麼任務,和任務對應的時間規則。spa
2. 關於任務, 這個角色是一個任務的執行者, 定時調度者 告訴 任務執行者 在何時開始執行和結束任務,其自己不會關注調度的實現。.net
3. 關於託管服務,也就是已經說過的Generic Host,固然你也可使用windows服務等。它的職責就是保證給任務提供執行環境,並告訴任務定時器當前服務在何時開始運行和關閉。 實現時提供了統一 IHostedService 接口,具體實現下邊實現會有展現。Generic Host 啓動方式有兩種形式:
a. 若是是.NetCore 站點,默認已經包含,只須要在 ConfigureServices 時註冊具體實現便可。
b. 能夠獨立建立,好比控制檯經過 new HostBuilder() 形式啓動,具體參見官方文檔。
爲了更直觀的展現相關之間的關係,這裏我畫了個類圖來分解相關的職責,同時也是後邊具體實現的主要內容:
二. 封裝實現
從上邊類圖能夠看出當前基礎框架主要由 BaseJobTrigger(觸發器基類),IJobExcutor(任務執行者接口),ListJobExcutor<IType>(通用列表循環任務執行者基類)。下邊分別就上邊三者貼出具體實現。
1. BaseJobTrigger(觸發器基類),實現代碼以下:
public abstract class BaseJobTrigger : IHostedService, IDisposable { private Timer _timer; private readonly TimeSpan _dueTime; private readonly TimeSpan _periodTime; private readonly IJobExecutor _jobExcutor; /// <summary> /// 構造函數 /// </summary> /// <param name="dueTime">到期執行時間</param> /// <param name="periodTime">間隔時間</param> /// <param name="jobExcutor">任務執行者</param> protected BaseJobTrigger(TimeSpan dueTime, TimeSpan periodTime, IJobExecutor jobExcutor) { _dueTime = dueTime; _periodTime = periodTime; _jobExcutor = jobExcutor; } #region 計時器相關方法 private void StartTimerTrigger() { if (_timer == null) _timer = new Timer(ExcuteJob,_jobExcutor,_dueTime, _periodTime); else _timer.Change(_dueTime, _periodTime); } private void StopTimerTrigger() { _timer?.Change(Timeout.Infinite, Timeout.Infinite); } private void ExcuteJob(object obj) { try { var excutor = obj as IJobExecutor; excutor?.StartJob(); } catch (Exception e) { LogUtil.Error($"執行任務({nameof(GetType)})時出錯,信息:{e}"); } } #endregion /// <summary> /// 系統級任務執行啓動 /// </summary> /// <returns></returns> public virtual Task StartAsync(CancellationToken cancellationToken) { try { StartTimerTrigger(); } catch (Exception e) { LogUtil.Error($"啓動定時任務({nameof(GetType)})時出錯,信息:{e}"); } return Task.CompletedTask; } /// <summary> /// 系統級任務執行關閉 /// </summary> /// <returns></returns> public virtual Task StopAsync(CancellationToken cancellationToken) { try { _jobExcutor.StopJob(); StopTimerTrigger(); } catch (Exception e) { LogUtil.Error($"中止定時任務({nameof(GetType)})時出錯,信息:{e}"); } return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } }
這個主要是完成對定時器的封裝,StartAsync和StopAsync 爲 IHostService 系統服務接口,表示託管服務的開始和結束。
2. IJobExcutor(任務執行者接口)
public interface IJobExecutor { /// <summary> /// 開始任務 /// </summary> void StartJob(); /// <summary> /// 結束任務 /// </summary> void StopJob(); }
3. ListJobExcutor<IType>(通用列表循環任務執行者基類)
public abstract class ListJobExcutor<IType>
: IJobExecutor { /// <summary> /// 運行狀態 /// </summary> public bool IsRuning { get;protected set; }
/// <summary> /// 開始任務 /// </summary> public void StartJob() { // 任務依然在執行中,不須要再次喚起 if (IsRuning) return; IsRuning = true; IList<IType> list = null; // 結清實體list do { for (var i = 0; IsRuning && i < list?.Count;i++) { ExcuteItem(list[i],i); } list = GetExcuteSource(); } while (IsRuning && list?.Count > 0); IsRuning = false; }
public void StopJob() { IsRuning = false; } /// <summary> /// 獲取list數據源 /// </summary> /// <returns></returns> protected virtual IList<IType> GetExcuteSource() { return null; } /// <summary> /// 個體任務執行 /// </summary> /// <param name="item">單個實體</param> /// <param name="index">在數據源中的索引</param> protected virtual void ExcuteItem(IType item,int index) { } }
這個是通用列表循環執的基礎封裝,由於業務中須要定時處理的大可能是須要從數據庫或文件批量獲取數據,執行處理,例如到期提醒,定時清理超時訂單等場景。
其主要功能實現是 從 GetExcuteSource() 獲取執行數據源,循環並經過 ExcuteItem() 執行個體任務,直到沒有數據源返回,則這次任務執行結束,等待下次任務觸發。若是當次執行時間過長,超過計時器時間間隔,重複觸發時 當前任務還在進行中,則不作任何處理。若是數據量過大須要併發執行,子類能夠在 ExcuteItem 中異步處理。這樣既可保證併發順序執行。
三. 示例演示
以上三個元素就構成了當前定時任務的主要基礎框架,在實際處理一個任務的過程當中,咱們須要定義一個執行者(XXXJobExcutor),一個觸發器(XXXJobTrigger,構造函數傳入觸發時間,間隔,執行者)便可。這裏用兩個示例來作演示
1. 基礎任務處理
public class TestJobTrigger:BaseJobTrigger { public TestJobTrigger() : base(TimeSpan.Zero, TimeSpan.FromMinutes(10), new TestJobExcutor()) { } } public class TestJobExcutor : IJobExecutor { public void StartJob() { LogUtil.Info("執行任務!"); } public void StopJob() { LogUtil.Info("系統終止任務"); } }
以上實現了TestJobTrigger 作任務觸發器,十分鐘執行一次。TestJobExcutor 做爲具體執行者,作任務處理。啓動時只需在Startup.cs 中的ConfigureServices方法中添加以下代碼便可:
services.AddHostedService<TestJobTrigger>();
2. 列表循環處理
public class ListJobTrigger : BaseJobTrigger { public ListJobTrigger() : base(TimeSpan.Zero, TimeSpan.FromMinutes(10), new ListJobExcutor()) { } } public class ListJobExcutor : ListJobExcutor<string> { private int _page = 0; protected override IList<string> GetExcuteSource() { if (_page==0) { _page++; return new List<string>{ "1", "2", "3" }; } return null; } protected override void ExcuteItem(string item, int index) { LogUtil.Info(item); } }
這個示例定時獲取字符串列表,並打印。同樣在Startup中註冊便可。
四. 注意事項
1. 關於什麼時候使用定時任務的問題
之因此要說這個問題,是由於我看過很多同窗把定時任務這種方式當成萬能膠,哪裏有縫往哪貼,一個不行起兩個。其實有不少場景均可以經過其關聯事件加消息隊列來完成,好比發短信,接收發送請求後塞消息隊列並返回請求方接收成功,隊列消費者來負責和短信服務商接口交互。只有對一些對時間屬性有要求的處理,我們經過定時任務等處理,如.....會員生日提醒....
2. 關於框架元素在解決方案的引用放置
一個建議: IJobExcutor,ListJobExcutor<IType> 能夠放置在通用類庫中,BaseJobTrigger,由於其依賴IHostService 放置在站點目錄下比較合適。
3. 關於GenericHost的生存週期問題
若是你使用的是控制檯啓動,則此問題暫時能夠忽略。
若是你使用的是站點項目,而且仍是經過IIS啓動,那麼你可能要注意了,由於.net core 的站點自身是有HOST宿主處理,IIS是其上代理,其啓動關閉,端口映射等由IIS內部完成。因此其依然受限於IIS的閒置回收影響,當IIS閒置回收時,其後的.Net Host也會被一同關閉,須要有新的請求進來時纔會再次啓動。不過鑑於當前任務處理已經如此簡單,有個取巧的作法,實現一個站點自身的心跳檢測任務,IIS默認20分鐘回收,任務時間能夠設爲15分鐘(你也能夠設置IIS站點回收時間),固然若是你的任務若是沒有那麼嚴格的時間要求你也能夠不用處理,由於回收後一旦接受到新的請求,任務會再次發起。
若是你已經看到這裏,而且感受還行的話能夠在下方點個贊,或者也能夠關注個人公總號(見二維碼)
_________________________________________