這是個代碼寫得很爛的電商系統(tǒng),只要運(yùn)行一段時間,服務(wù)器就會出現(xiàn)Out Of Memory。
別人都忙得四腳朝天,于是實習(xí)生張大胖被抓了壯丁去研究為什么會出現(xiàn)OOM。
剛?cè)胄械膹埓笈旨夹g(shù)水平一般,“裝模作樣”地看代碼,研究日志,請教老員工,一個星期過去了,還是一無所獲。
周一例行的項目會議上, 大家似乎要看張大胖的笑話了,沒想到他卻提了一個歪招:“這個OOM問題非常復(fù)雜,一時半會兒也解決不了,要不我們定時重啟服務(wù)器怎么樣?”
一臉嚴(yán)肅的項目經(jīng)理老梁點點頭:“以目前的情況看,也只能如此了。但是不能讓服務(wù)中斷,這樣吧,公司有兩臺服務(wù)器,一臺在凌晨1點重啟, 另外一臺在凌晨2點重啟?!?/p>
得到了領(lǐng)導(dǎo)的首肯,張大胖趕緊行動,周末他其實已經(jīng)做了準(zhǔn)備,研究了Linux上的crontab,它的格式是這樣樣子:
每天凌晨一點重啟系統(tǒng),可以這么寫:
0 1 * * * restart.sh
(碼農(nóng)翻身注:這里只是個簡單的例子, 實際上crontab及其靈活)
這個OOM的問題被張大胖靈機(jī)一動給解決了,或者說,被臨時隱藏了。
大家知道張大胖擅長crontab, 都把一些定時的任務(wù)扔給他去做: 什么定時統(tǒng)計報表,定時同步數(shù)據(jù),定時刪除表中的無效訂單...... 等等。
張大胖整天面對的就是crontab和腳本,都快要吐了。
不僅如此,同事們還經(jīng)常提出一些“變態(tài)”的需求:
“大胖,那個定時任務(wù)運(yùn)行得怎么樣了?”
“大胖,我想把那個定時任務(wù)給停掉?!?/p>
“大胖,那個定時任務(wù)今晚別運(yùn)行啊!”
“......”
張大胖真是煩死了,他心想,要是提供個界面讓大家使用就好了, 可是crontab似乎并不支持。
要不自己開發(fā)一個?
有一次張大胖偶然發(fā)現(xiàn)了JDK中的Timer類,似乎也是做這些定時任務(wù)的, 不由地眼前一亮,但是仔細(xì)研究以后就發(fā)現(xiàn),JDK的Timer還是太簡單了,做點簡單的定時任務(wù)還行, 對于復(fù)雜的情況,尤其是復(fù)雜的時間策略,還是力不從心。
看來自己需要從頭設(shè)計了,張大胖想到了公眾號碼農(nóng)翻身的一篇文章《一個著名的日志系統(tǒng)是怎么設(shè)計出來的?》, 小張用“正交”的原則設(shè)計出了Logger, Appender, Formatter這些類。
我也可以使用同樣的原則啊,小張能行,我憑什么不行?
說干就干,先想想需求,非常簡單,不就是定時地執(zhí)行任務(wù)嘛!
“任務(wù)”應(yīng)該是正交中的一個“維度”,我可以抽象出一個接口叫做Task , 嗯,還是叫做Job吧。
對使用者來說,他需要提供一個實現(xiàn)類出來,在實現(xiàn)類中描述要做什么事情,比如:生成報表,復(fù)制數(shù)據(jù)......
“定時”該怎么處理? 定時,定時觸發(fā),干脆叫做Trigger吧。
這個Trigger 可以指定什么時間開始,時間間隔,運(yùn)行多少次, 能覆蓋大部分需求了。
可是張大胖轉(zhuǎn)念一想,如果有人要求類似日歷的重復(fù)間隔該怎么處理? 比如每月的第一天運(yùn)行,或者每周的最后一天運(yùn)行,該怎么辦? crontab特別適合描述這種情況,對,可以搞一個類似于crontab的Trigger。
看來Trigger最好也是個接口,我來提供幾個默認(rèn)的實現(xiàn),比如SimpleTrigger,CronTrigger,用戶還可以擴(kuò)展,這樣就靈活了。
Job和Trigger也是正交的關(guān)系, 兩者可以互不影響,可以獨立擴(kuò)展,真是不錯, 張大胖不僅得意起來,這設(shè)計也很簡單嘛!
但是怎么把這兩個家伙結(jié)合起來?
必須得有個“大管家”才行,這個大管家應(yīng)該可以接受Job, 然后按照各種Trigger去運(yùn)行,嗯,叫做調(diào)度器Scheduler應(yīng)該不錯。
張大胖畫了個草圖,來展示三者之間的關(guān)系:
設(shè)計得差不多了,可以進(jìn)入開發(fā)階段了, 因為是自己要寫一個類似于框架的東西,讓別人去使用,張大胖開發(fā)起來非常有激情,即使是利用晚上和周末的時間來寫代碼,也是像打了雞血一樣,根本不覺得累。
一個月過去了,第一版新鮮出爐。
這個版本不僅有核心的API像Job, Trigger, Scheduler ,張大胖還專門開發(fā)了一個界面,用來展示定時任務(wù)的進(jìn)展,例如什么時間運(yùn)行,運(yùn)行了幾次,失敗了幾次......等等。
張大胖把它叫做“大胖定時任務(wù)調(diào)度系統(tǒng)”。
他興奮地拿去讓項目經(jīng)理老梁看, 可是老梁并不感冒,面無表情地說:“你這個小軟件有啥用啊?!?/p>
張大胖被潑了一盆冷水,依然熱情滿滿地推銷:“用了我的這個定時調(diào)度系統(tǒng),任何人都可以輕松地啟動,停止任務(wù), 咱們項目中所有的定時任務(wù)一目了然。 大家就不用找我來手工調(diào)整了?!?/p>
老梁開玩笑地說:“奧,那你的實習(xí)工作就可以結(jié)束了,哈哈?!?/p>
正巧CTO Bill經(jīng)過,他饒有興趣地看了一會,提了一個問題:“假設(shè)你這個大胖調(diào)度系統(tǒng)在運(yùn)行的時候,機(jī)器突然間Down掉了,怎么處理?”
張大胖一臉懵逼:“什么怎么處理,重啟機(jī)器唄。”
Bill 說: “之前的任務(wù)還能接著運(yùn)行嗎,比如說一個任務(wù)需要運(yùn)行100次,在機(jī)器down掉之前運(yùn)行了90次,重啟后能不能從第91次運(yùn)行?”
張大胖有點發(fā)窘,不好意思地?fù)蠐项^:“這一點我還真沒考慮到,我現(xiàn)在都是在內(nèi)存中記錄運(yùn)行的情況,看來得做持久化了?!?/p>
Bill 聽到持久化這個詞,知道張大胖已經(jīng)Get到了,他說,你把這個持久化實現(xiàn)了,到時候直接向我匯報。
得到了CTO的賞識,張大胖不敢怠慢,趕緊進(jìn)行新的設(shè)計, 他抽象了一個叫做JobStore的接口,表示Job的存儲,像什么Job,Trigger, Job運(yùn)行情況都存儲在其中。
下面有兩個實現(xiàn),分別對應(yīng)內(nèi)存存儲和數(shù)據(jù)庫存儲。
雖然SQL是標(biāo)準(zhǔn)的,但是不同的數(shù)據(jù)庫還是有細(xì)微的差異, 張大胖覺得得把這些差異給封裝起來, 他又提取了一個接口叫做DriverDelegate, 屏蔽了數(shù)據(jù)庫細(xì)節(jié),讓DbJobStore使用。
他還提供了一個缺省的實現(xiàn)StdJDBCDelegate,如果那些數(shù)據(jù)庫還有獨特的實現(xiàn),那就寫個子類就行了。
“大胖定時任務(wù)調(diào)度系統(tǒng) 2.0” 開發(fā)完成以后,張大胖仔細(xì)地想了一遍,似乎沒有什么漏洞了,決定正式向CTO Bill去匯報。
Bill 親切地詢問了張大胖加班加點設(shè)計和開發(fā)的情況,對他這種不計較個人得失,一心一意為公司謀福利的精神表示了高度的贊賞。
張大胖受寵若驚。
Bill話鋒一轉(zhuǎn):“我們的系統(tǒng)最近用戶越來越多,老板特別提出了高可用的需求,系統(tǒng)的各個組件也得達(dá)到高可用!”
“高可用? 拿我的定時調(diào)度系統(tǒng)來說,就是說可以部署在多個機(jī)器上,一個down掉了,其他的還可以運(yùn)行,對吧?” 張大胖一點就透。
Bill 贊許地點點頭:“你想好怎么去實現(xiàn)了嗎?”
“很簡單啊,把定時調(diào)度系統(tǒng)部署到多個機(jī)器上,形成幾個備份就行了!”
張大胖還在白板上畫了這么一個圖:
“那同一個時刻,有多少個Scheduler 在運(yùn)行?” Bill 終于拋出了重磅炸彈。
張大胖現(xiàn)在明白Bill的疑問了了,三個實例都在運(yùn)行,那一個Job就有可能運(yùn)行多次,這肯定是不行的!
他說道:“要不讓三個實例A,B,C都去訪問同一個數(shù)據(jù)庫吧!”
Bill說:“那三個實例訪問同一份數(shù)據(jù),肯定會出現(xiàn)沖突,互相覆蓋,那就亂套了!”
其實,實例A,實例B,實例C組成一個類似集群的東西,但是同一時刻,一個Job只能在一個實例上運(yùn)行。
比如Job X 從凌晨1點開始,每隔1小時運(yùn)行一次,那1:00 的時候Job X可能在實例A上運(yùn)行, 2:00的時候可能在實例B上運(yùn)行, 3:00的時候可能在實例C上運(yùn)行。
也就是說,這三個實例部分地實現(xiàn)了負(fù)載均衡。
張大胖說:“這可就難辦了。難道讓這三個實例A,B,C之間互相通信?”
Bill說道:“那樣有點麻煩,就變成一個分布式系統(tǒng)下的通信問題了,我們要不用這個數(shù)據(jù)庫做點文章? 反正這個數(shù)據(jù)庫已經(jīng)存了Job的信息,Trigger的信息,我們就多加一個表吧,就叫LOCKS,這個表里邊每一行記錄都可以當(dāng)做一個‘鎖’來用?!?/p>
張大胖表示不太明白。
“很簡單,就是數(shù)據(jù)庫的‘行’鎖嘛, 比如SELECT * FROM LOCKS where LOCK_NAME='TRIGGER' FOR UPDATE ,這就把那一行記錄給鎖住了, 別的事務(wù)只能等待當(dāng)前事務(wù)commit以后才能訪問。”
張大胖還是不太明白。
“比如,服務(wù)器A的實例A在一個事務(wù)中先執(zhí)行了上面SQL, 就把那一行給鎖住了,當(dāng)服務(wù)器B的實例B也去執(zhí)行同樣的SQL的時候, 只能等待,對吧? 這不就相當(dāng)于實例A獲得了鎖嗎?”
“原來如此,以后任何一個調(diào)度器實例想要獲取Job的運(yùn)行時間,設(shè)置Job的下一次運(yùn)行時間的時候,都必須先獲得這個鎖。這樣這些分布式的調(diào)度器就不會沖突了,只會運(yùn)行一個特定時間的Job。 我這就去做個詳細(xì)設(shè)計,再來匯報?!?/p>
兩個月后,“大胖定時任務(wù)調(diào)度系統(tǒng) 3.0” 開發(fā)完畢,在Bill的大力支持和推動下,成功地應(yīng)用在了公司的項目中。
靈活的設(shè)計和擴(kuò)展性,加上持久化,集群等強(qiáng)大的功能, 系統(tǒng)受到了大家的歡迎。
考慮到很多公司都會有類似的需求,Bill決定把系統(tǒng)開源, 只是“大胖定時任務(wù)調(diào)度系統(tǒng)”這個名字有點俗,還有點長,Bill把它改名為“Quartz”。
Quartz從此流行開來。
(注:本文試圖講解Quartz的原理,其中的類圖并沒有和Quartz的真實類完全對應(yīng),這一點請大家知曉。)
(完)