我重構定時任務服務時,運用的那些編程思想

在重構一個老項目的一個定時任務服務的過程當中,我想到了幾個有趣的點子,整個服務的骨架就是借鑑這幾個點子搭建的。spring

定時任務服務改造的幾個階段

一開始想作的,只是能讓定時任務實現可頁面配置,可隨時修改配置隨時生效。配置指的是配置cron表達式,定義任務的執行時機。但因爲後期的種種問題,不得不對定時任務服務進行再次改造,因此,定時任務服務經歷了三個階段。數據庫

第一個階段:
目的:定時任務作成可配置。
缺點:發現定時任務都很耗內存,且因爲執行時間過長,一般幾分鐘的都有,這樣就會有任務碰撞到一塊兒執行的狀況,至少CPU長期百分百使用狀態。好比報表統計類任務,大多定義在每一個小時的前10分鐘內完成。編程

第二個階段:
目的:減小內存和下降CPU的使用率。
方案:將定時任務串行化執行,由一個單一線程的線程池去執行。
缺點:將任務串行化執行後,會有風險。好比因某個卡住了,致使後面的任務都得不到執行。設計模式

第三階段:
目的:解決串行化執行的弊端。
方案:引入監視器。若是有任務從提交到執行,時間超過15分鐘還未完成,就直接中斷線程,讓下個任務可以得以執行,併發送郵件通知便於排查緣由。緩存

每一個階段借鑑的思想

一、引導器

以前學彙編的時候知道操做系統有個引導器的存在,就是在系統盤的某個開始位置,由主板上的程序加載執行,系統再由引導器啓動。定時任務也應該有一個啓動器來初始化配置並提交到調度線程池,因此我借鑑了系統引導器的設計。多線程

要求全部定時任務都實現定時任務接口TimeTaskPlayer。由於調度線程池要求submit是一個Runnable,因此定時任務接口要繼承Runnable接口,由 run 方法調用子類實現的startPlayer方法。至於爲何不讓子類(定時任務)直接實現run方法,後面會有用處。併發

我重構定時任務服務時,運用的那些編程思想

定義引導器接口jvm

我重構定時任務服務時,運用的那些編程思想

實現定時任務啓動引導器ide

我重構定時任務服務時,運用的那些編程思想

在Spring boot初始完成後,調用引導器初始化服務編碼

我重構定時任務服務時,運用的那些編程思想

固然,優雅退出確定也不能少呀,其實能夠直接使用spring的優雅退出的,都是使用的同一個原理,註冊jvm鉤子。

我重構定時任務服務時,運用的那些編程思想

提供一個全部任務的ScheduleFuture的持有者,提供中止全部任務的方法,用於更新配置後取消全部定時任務,由引導器從新啓動。即更新配置後重啓全部定時任務。

我重構定時任務服務時,運用的那些編程思想

任務的Cron表達式配置管理類,提供reloadCronFromDB方法給接口調用更新任務的cron表達式緩存。這裏的註釋有改動,存的不是完整類名,並且去掉包名後的類名,同時Bean的name(spring管理)也是去掉包名後的類名,首字母大寫。

我重構定時任務服務時,運用的那些編程思想

三個方法很好理解,一個是根據定時任務的Class獲取cron表達式,若是緩存沒有,則從數據庫加載。第二個是獲取定時任務的狀態,用於控制是否啓用這個定時任務。

固然,還有使用Aop添加任務執行異常郵件通知,這裏就不貼了。

二、插件

如何將定時任務控制串行執行,且不改動現有代碼呢,若是改動太大就至關於重構了。這時候我想到了插件。插件咱們經常用到,好比idea就有不少插件,再與咱們貼近點的就是Mybatis的分頁插件。插件,無外呼就是在某些任務開始以前插入埋點代碼,其實也是AOP編程思想。因此我借鑑了插件這一思想,來實現不修改現有代碼的狀況下將定時任務串行執行。這裏使用了觀察者模式。

觀察者模式:抽象觀察者

我重構定時任務服務時,運用的那些編程思想

觀察者模式:抽象主題

我重構定時任務服務時,運用的那些編程思想

觀察者模式:具體的定時任務事件執行者,即觀察者。這裏包含了監聽器的內容,就是將事件轉爲任務放入單線程的線程池後,拿到Future,交給監聽器監控任務的執行狀態。

我重構定時任務服務時,運用的那些編程思想

我重構定時任務服務時,運用的那些編程思想

觀察者模式:具體的事件主題,接收事件並通知對該事件感興趣的觀察者。

我重構定時任務服務時,運用的那些編程思想

那麼,什麼時候發佈的事件呢?就是定時任務到執行時間的時候。文章開頭就埋下了一個點,就是定時任務接口TimedTaskPlayer爲什麼不讓子類直接實現run方法,爲的就是能夠在不改任務代碼的狀況下,實現讓定時任務改成串行執行。

修改後的TimedTaskPlayer接口以下圖,注意看run方法,神不知鬼不覺的就能將任務的執行權轉交出去。定時任務就只是一個任務的執行時間節點的掌控者,再也不是任務執行的掌控者,簡簡單單的就被抽空了身體。

我重構定時任務服務時,運用的那些編程思想

三、監視器

如何杜絕串行任務因單個任務阻塞致使服務崩潰呢?當咱們使用idea編碼的時候,因打開的軟件太多,就會致使系統變卡,可是咱們能夠經過系統進程監視器看到idea卡住了,咱們能夠選擇手動殺掉重啓。

因此,我想個人定時任務系統也能有這樣的功能。加入監視器,在任務提交到單線程線程池時,也將返回的Future提交到監視隊列,由監視器線程輪詢隊列中任務的執行狀況,發現超時未執行完的任務直接中斷執行,不然將任務放入監視隊列末尾。這裏的超時目前我只能拿任務的提交時間和當前時間計算。

我重構定時任務服務時,運用的那些編程思想

變種的設計模式之策略模式

定時任務模塊中還有一個消息訂閱消費的小模塊,固然這與定時任務沒有關係。這裏我用到了一種設置模式,叫條件執行器。啥?正如過濾器與攔截器是責任鏈的一種變種同樣,條件執行器也是策略模式的一種變種,固然條件執行器是我亂叫的。

爲啥叫條件執行器,在使用switch分支語句的時候,咱們能夠定義case一、二、3執行某個邏輯,case4執行某個邏輯。同樣的,一條消息可能會有不少條件執行器感興趣,也可能沒有任何條件執行器感興趣,也可能只有一個條件執行器感興趣。與switch很像,因此我叫它條件執行器。固然,這類消息屬於通知類消息,不管消費成功或失敗,都不會再有第二次消費。

總結

定時任務串行化執行有風險,但倒是爲了能在4g內存的機器上跑起來。可是,若是出現有任務把線程堵住的狀況,那就是代碼有問題,若是是代碼的問題,即使是多線程,風險同樣存在,甚至更高。爲什麼這個說,假如一個任務3分鐘執行一次,結果每次都把線程堵住,要麼把內存玩爆,要麼把線程池隊列阻塞滿,最後還不是同樣的下場。

固然,並不是全部業務場景都適用,若是對定時任務要求及時的,就不能這麼用,好比我必定要讓這個任務0點0分執行。或者當任務愈來愈多的時候,好比有上百個,上百個任務串行執行想下什麼後果。

相關文章
相關標籤/搜索