1. 程式人生 > 程式設計 >我重構定時任務服務時,運用的那些程式設計思想

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

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

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

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

第一個階段: 目的:定時任務做成可配置。 缺點:發現定時任務都很耗記憶體,且由於執行時間過長,通常幾分鐘的都有,這樣就會有任務碰撞到一起執行的情況,至少CPU長期百分百使用狀態。比如報表統計類任務,大多定義在每個小時的前10分鐘內完成。

第二個階段: 目的:減少記憶體和降低CPU的使用率。 方案:將定時任務序列化執行,由一個單一執行緒的執行緒池去執行。 缺點:將任務序列化執行後,會有風險。比如因某個卡住了,導致後面的任務都得不到執行。

第三階段: 目的:解決序列化執行的弊端。 方案:引入監視器。如果有任務從提交到執行,時間超過15分鐘還未完成,就直接中斷執行緒,讓下個任務能夠得以執行,併傳送郵件通知便於排查原因。

每個階段借鑑的思想

1、引導器

之前學彙編的時候知道作業系統有個引導器的存在,就是在系統盤的某個開始位置,由主機板上的程式載入執行,系統再由引導器啟動。定時任務也應該有一個啟動器來初始化配置並提交到排程執行緒池,所以我借鑑了系統引導器的設計。

要求所有定時任務都實現定時任務介面TimeTaskPlayer。因為排程執行緒池要求submit是一個Runnable,所以定時任務介面要繼承Runnable介面,由 run 方法呼叫子類實現的startPlayer方法。至於為什麼不讓子類(定時任務)直接實現run方法,後面會有用處。

定義引導器介面

實現定時任務啟動引導器

在Spring boot初始完成後,呼叫引導器初始化服務

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

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

任務的Cron表示式配置管理類,提供reloadCronFromDB方法給介面呼叫更新任務的cron表示式快取。這裡的註釋有改動,存的不是完整類名,而且去掉包名後的類名,同時Bean的name(spring管理)也是去掉包名後的類名,首字母大寫。

三個方法很好理解,一個是根據定時任務的Class獲取cron表示式,如果快取沒有,則從資料庫載入。第二個是獲取定時任務的狀態,用於控制是否啟用這個定時任務。

當然,還有使用Aop新增任務執行異常郵件通知,這裡就不貼了。

2、外掛

如何將定時任務控制序列執行,且不改動現有程式碼呢,如果改動太大就相當於重構了。這時候我想到了外掛。外掛我們常常用到,比如idea就有很多外掛,再與我們貼近點的就是Mybatis的分頁外掛。外掛,無外呼就是在某些任務開始之前插入埋點程式碼,其實也是AOP程式設計思想。所以我借鑑了外掛這一思想,來實現不修改現有程式碼的情況下將定時任務序列執行。這裡使用了觀察者模式。

觀察者模式:抽象觀察者

觀察者模式:抽象主題

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

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

那麼,何時釋出的事件呢?就是定時任務到執行時間的時候。文章開頭就埋下了一個點,就是定時任務介面TimedTaskPlayer為何不讓子類直接實現run方法,為的就是可以在不改任務程式碼的情況下,實現讓定時任務改為序列執行。

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

3、監視器

如何杜絕序列任務因單個任務阻塞導致服務崩潰呢?當我們使用idea編碼的時候,因開啟的軟體太多,就會導致系統變卡,但是我們可以通過系統程式監視器看到idea卡住了,我們可以選擇手動殺掉重啟。

所以,我想我的定時任務系統也能有這樣的功能。加入監視器,在任務提交到單執行緒執行緒池時,也將返回的Future提交到監視佇列,由監視器執行緒輪詢佇列中任務的執行情況,發現超時未執行完的任務直接中斷執行,否則將任務放入監視佇列末尾。這裡的超時目前我只能拿任務的提交時間和當前時間計算。

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

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

為啥叫條件執行器,在使用switch分支語句的時候,我們可以定義case1、2、3執行某個邏輯,case4執行某個邏輯。一樣的,一條訊息可能會有很多條件執行器感興趣,也可能沒有任何條件執行器感興趣,也可能只有一個條件執行器感興趣。與switch很像,所以我叫它條件執行器。當然,這類訊息屬於通知類訊息,無論消費成功或失敗,都不會再有第二次消費。

總結

定時任務序列化執行有風險,但卻是為了能在4g記憶體的機器上跑起來。但是,如果出現有任務把執行緒堵住的情況,那就是程式碼有問題,如果是程式碼的問題,即便是多執行緒,風險一樣存在,甚至更高。為何這個說,假如一個任務3分鐘執行一次,結果每次都把執行緒堵住,要麼把記憶體玩爆,要麼把執行緒池佇列阻塞滿,最後還不是一樣的下場。

當然,並非所有業務場景都適用,如果對定時任務要求及時的,就不能這麼用,比如我一定要讓這個任務0點0分執行。或者當任務越來越多的時候,比如有上百個,上百個任務序列執行想下什麼後果。