在重構(gòu)一個(gè)老項(xiàng)目的一個(gè)定時(shí)任務(wù)服務(wù)的過程中,我想到了幾個(gè)有趣的點(diǎn)子,整個(gè)服務(wù)的骨架就是借鑒這幾個(gè)點(diǎn)子搭建的。
創(chuàng)新互聯(lián):2013年至今為各行業(yè)開拓出企業(yè)自己的“網(wǎng)站建設(shè)”服務(wù),為上千余家公司企業(yè)提供了專業(yè)的成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)、網(wǎng)頁設(shè)計(jì)和網(wǎng)站推廣服務(wù), 按需定制設(shè)計(jì)由設(shè)計(jì)師親自精心設(shè)計(jì),設(shè)計(jì)的效果完全按照客戶的要求,并適當(dāng)?shù)奶岢龊侠淼慕ㄗh,擁有的視覺效果,策劃師分析客戶的同行競爭對手,根據(jù)客戶的實(shí)際情況給出合理的網(wǎng)站構(gòu)架,制作客戶同行業(yè)具有領(lǐng)先地位的。
一開始想做的,只是能讓定時(shí)任務(wù)實(shí)現(xiàn)可頁面配置,可隨時(shí)修改配置隨時(shí)生效。配置指的是配置cron表達(dá)式,定義任務(wù)的執(zhí)行時(shí)機(jī)。但由于后期的種種問題,不得不對定時(shí)任務(wù)服務(wù)進(jìn)行再次改造,所以,定時(shí)任務(wù)服務(wù)經(jīng)歷了三個(gè)階段。
第一個(gè)階段:
目的:定時(shí)任務(wù)做成可配置。
缺點(diǎn):發(fā)現(xiàn)定時(shí)任務(wù)都很耗內(nèi)存,且由于執(zhí)行時(shí)間過長,通常幾分鐘的都有,這樣就會有任務(wù)碰撞到一起執(zhí)行的情況,至少CPU長期百分百使用狀態(tài)。比如報(bào)表統(tǒng)計(jì)類任務(wù),大多定義在每個(gè)小時(shí)的前10分鐘內(nèi)完成。
第二個(gè)階段:
目的:減少內(nèi)存和降低CPU的使用率。
方案:將定時(shí)任務(wù)串行化執(zhí)行,由一個(gè)單一線程的線程池去執(zhí)行。
缺點(diǎn):將任務(wù)串行化執(zhí)行后,會有風(fēng)險(xiǎn)。比如因某個(gè)卡住了,導(dǎo)致后面的任務(wù)都得不到執(zhí)行。
第三階段:
目的:解決串行化執(zhí)行的弊端。
方案:引入監(jiān)視器。如果有任務(wù)從提交到執(zhí)行,時(shí)間超過15分鐘還未完成,就直接中斷線程,讓下個(gè)任務(wù)能夠得以執(zhí)行,并發(fā)送郵件通知便于排查原因。
之前學(xué)匯編的時(shí)候知道操作系統(tǒng)有個(gè)引導(dǎo)器的存在,就是在系統(tǒng)盤的某個(gè)開始位置,由主板上的程序加載執(zhí)行,系統(tǒng)再由引導(dǎo)器啟動。定時(shí)任務(wù)也應(yīng)該有一個(gè)啟動器來初始化配置并提交到調(diào)度線程池,所以我借鑒了系統(tǒng)引導(dǎo)器的設(shè)計(jì)。
要求所有定時(shí)任務(wù)都實(shí)現(xiàn)定時(shí)任務(wù)接口TimeTaskPlayer。因?yàn)檎{(diào)度線程池要求submit是一個(gè)Runnable,所以定時(shí)任務(wù)接口要繼承Runnable接口,由 run 方法調(diào)用子類實(shí)現(xiàn)的startPlayer方法。至于為什么不讓子類(定時(shí)任務(wù))直接實(shí)現(xiàn)run方法,后面會有用處。
定義引導(dǎo)器接口
實(shí)現(xiàn)定時(shí)任務(wù)啟動引導(dǎo)器
在Spring boot初始完成后,調(diào)用引導(dǎo)器初始化服務(wù)
當(dāng)然,優(yōu)雅退出肯定也不能少呀,其實(shí)可以直接使用spring的優(yōu)雅退出的,都是使用的同一個(gè)原理,注冊jvm鉤子。
提供一個(gè)所有任務(wù)的ScheduleFuture的持有者,提供停止所有任務(wù)的方法,用于更新配置后取消所有定時(shí)任務(wù),由引導(dǎo)器重新啟動。即更新配置后重啟所有定時(shí)任務(wù)。
任務(wù)的Cron表達(dá)式配置管理類,提供reloadCronFromDB方法給接口調(diào)用更新任務(wù)的cron表達(dá)式緩存。這里的注釋有改動,存的不是完整類名,而且去掉包名后的類名,同時(shí)Bean的name(spring管理)也是去掉包名后的類名,首字母大寫。
三個(gè)方法很好理解,一個(gè)是根據(jù)定時(shí)任務(wù)的Class獲取cron表達(dá)式,如果緩存沒有,則從數(shù)據(jù)庫加載。第二個(gè)是獲取定時(shí)任務(wù)的狀態(tài),用于控制是否啟用這個(gè)定時(shí)任務(wù)。
當(dāng)然,還有使用Aop添加任務(wù)執(zhí)行異常郵件通知,這里就不貼了。
如何將定時(shí)任務(wù)控制串行執(zhí)行,且不改動現(xiàn)有代碼呢,如果改動太大就相當(dāng)于重構(gòu)了。這時(shí)候我想到了插件。插件我們常常用到,比如idea就有很多插件,再與我們貼近點(diǎn)的就是Mybatis的分頁插件。插件,無外呼就是在某些任務(wù)開始之前插入埋點(diǎn)代碼,其實(shí)也是AOP編程思想。所以我借鑒了插件這一思想,來實(shí)現(xiàn)不修改現(xiàn)有代碼的情況下將定時(shí)任務(wù)串行執(zhí)行。這里使用了觀察者模式。
觀察者模式:抽象觀察者
觀察者模式:抽象主題
觀察者模式:具體的定時(shí)任務(wù)事件執(zhí)行者,即觀察者。這里包含了監(jiān)聽器的內(nèi)容,就是將事件轉(zhuǎn)為任務(wù)放入單線程的線程池后,拿到Future,交給監(jiān)聽器監(jiān)控任務(wù)的執(zhí)行狀態(tài)。
觀察者模式:具體的事件主題,接收事件并通知對該事件感興趣的觀察者。
那么,何時(shí)發(fā)布的事件呢?就是定時(shí)任務(wù)到執(zhí)行時(shí)間的時(shí)候。文章開頭就埋下了一個(gè)點(diǎn),就是定時(shí)任務(wù)接口TimedTaskPlayer為何不讓子類直接實(shí)現(xiàn)run方法,為的就是可以在不改任務(wù)代碼的情況下,實(shí)現(xiàn)讓定時(shí)任務(wù)改為串行執(zhí)行。
修改后的TimedTaskPlayer接口如下圖,注意看run方法,神不知鬼不覺的就能將任務(wù)的執(zhí)行權(quán)轉(zhuǎn)交出去。定時(shí)任務(wù)就只是一個(gè)任務(wù)的執(zhí)行時(shí)間節(jié)點(diǎn)的掌控者,不再是任務(wù)執(zhí)行的掌控者,簡簡單單的就被抽空了身體。
如何杜絕串行任務(wù)因單個(gè)任務(wù)阻塞導(dǎo)致服務(wù)崩潰呢?當(dāng)我們使用idea編碼的時(shí)候,因打開的軟件太多,就會導(dǎo)致系統(tǒng)變卡,但是我們可以通過系統(tǒng)進(jìn)程監(jiān)視器看到idea卡住了,我們可以選擇手動殺掉重啟。
所以,我想我的定時(shí)任務(wù)系統(tǒng)也能有這樣的功能。加入監(jiān)視器,在任務(wù)提交到單線程線程池時(shí),也將返回的Future提交到監(jiān)視隊(duì)列,由監(jiān)視器線程輪詢隊(duì)列中任務(wù)的執(zhí)行情況,發(fā)現(xiàn)超時(shí)未執(zhí)行完的任務(wù)直接中斷執(zhí)行,否則將任務(wù)放入監(jiān)視隊(duì)列末尾。這里的超時(shí)目前我只能拿任務(wù)的提交時(shí)間和當(dāng)前時(shí)間計(jì)算。
定時(shí)任務(wù)模塊中還有一個(gè)消息訂閱消費(fèi)的小模塊,當(dāng)然這與定時(shí)任務(wù)沒有關(guān)系。這里我用到了一種設(shè)置模式,叫條件執(zhí)行器。啥?正如過濾器與攔截器是責(zé)任鏈的一種變種一樣,條件執(zhí)行器也是策略模式的一種變種,當(dāng)然條件執(zhí)行器是我亂叫的。
為啥叫條件執(zhí)行器,在使用switch分支語句的時(shí)候,我們可以定義case1、2、3執(zhí)行某個(gè)邏輯,case4執(zhí)行某個(gè)邏輯。一樣的,一條消息可能會有很多條件執(zhí)行器感興趣,也可能沒有任何條件執(zhí)行器感興趣,也可能只有一個(gè)條件執(zhí)行器感興趣。與switch很像,所以我叫它條件執(zhí)行器。當(dāng)然,這類消息屬于通知類消息,無論消費(fèi)成功或失敗,都不會再有第二次消費(fèi)。
定時(shí)任務(wù)串行化執(zhí)行有風(fēng)險(xiǎn),但卻是為了能在4g內(nèi)存的機(jī)器上跑起來。但是,如果出現(xiàn)有任務(wù)把線程堵住的情況,那就是代碼有問題,如果是代碼的問題,即便是多線程,風(fēng)險(xiǎn)一樣存在,甚至更高。為何這個(gè)說,假如一個(gè)任務(wù)3分鐘執(zhí)行一次,結(jié)果每次都把線程堵住,要么把內(nèi)存玩爆,要么把線程池隊(duì)列阻塞滿,最后還不是一樣的下場。
當(dāng)然,并非所有業(yè)務(wù)場景都適用,如果對定時(shí)任務(wù)要求及時(shí)的,就不能這么用,比如我一定要讓這個(gè)任務(wù)0點(diǎn)0分執(zhí)行?;蛘弋?dāng)任務(wù)越來越多的時(shí)候,比如有上百個(gè),上百個(gè)任務(wù)串行執(zhí)行想下什么后果。