一次quartz非典型異常排查記錄

一次quartz非典型異常排查記錄

問題
本地部署定時器任務能正常執行,發佈到測試環境就不執行.

排查
通過觀察數據庫表triggers,週期任務若執行成功,其TRIGGER_STATE字段的值爲WAITING,反之則爲ERROR,當時覺着關鍵就在這了,於是網上搜索ERROR的原因.有一個說法我比較認同,說是有不止一個應用實例在操作quartz數據庫!我想想,原因可能是這樣:假設A實例往庫裏插入了定時任務,觸發器到點了卻被B實例(或C,D,E,F,G…)觸發;

比如我在A實例代碼裏封裝的JobDataMap和JobDetail,此時B實例欲執行定時任務必須先拿到在A實例封裝好的JobDetail,然而B實例嘗試去獲取的時候就會出錯,因爲在反序列化JobDataMap對象時,quartz底層會發現B實例並沒有DetectionParam,DetectionContext等對象或屬性!

解決方案
方案一
讓A實例連接一個特定的quartz數據庫,保證A實例創建的定時任務只能由A觸發,這種方案驗證可行.然而不靈活,如果存在n個實例,也要配置n個quartz數據庫;

方案二
*思考:*爲什麼A實例創建的定時任務能被連接同一個庫的別的應用實例觸發?

假設1:quartz有一套輪詢機制,類似Nginx的負載均衡一樣;這種解釋說得通,但仔細一想就有問題,誰來充當分發任務的媒介?每個quartz都是獨立部署,沒有管理每個quartz的第三方應用存在;

*假設2:*每個應用實例都會開啓一個線程不斷地去訪問數據庫,找出即將滿足觸發條件的定時任務;如果假設2成立,那麼我們只要控制每個實例只能從數據庫裏查詢到屬於自己創建的定時任務就行了;

接下來就是驗證假設2是否成立,這得深入源碼去探個究竟:

步驟:
1.既然是線程,規範的命名是以Thread結尾,於是打開quartz-2.3.0jar包,翻開目錄,很快就發現有QuartzSchedulerThread這麼一個類,如圖:
在這裏插入圖片描述
2.進入這個類,看到有這麼一段註釋,意思是這是一個執行QuartzScheduler註冊的觸發器的線程:
在這裏插入圖片描述
3.既然要執行觸發器,首先得從數據庫裏查詢出滿足條件的觸發器,進入run()方法內部,有這麼一段代碼:
在這裏插入圖片描述
4.見名知意,List集合裝的泛型類型翻譯過來是"可操作的觸發器",那麼triggers就表示所有查詢出來的待執行觸發器;紅色方框的那段代碼繼續跟蹤下去會發現最終執行的是StdJDBCConstants中的這段sql語句:
在這裏插入圖片描述
5.將當前時間和triggers表中所有記錄的NEXT_FIRE_TIME字段值進行比較,如果大於該時間則執行該trrigers記錄對應的任務,並且COL_SCHEDULER_NAME = SCHED_NAME_SUBST,定位 SCHED_NAME_SUBST這個常量值,發現它是長這樣的:
在這裏插入圖片描述
很明顯,這個值是讀取配置文件得來的,而我們起初在quartz.properties文件中並沒有配置SCHEDULER_NAME,所以這個字段存到數據庫裏是有一個默認值:QuartzScheduler!到這裏,就不難理解爲什麼A實例創建的定時任務會被別的實例給觸發,原因是它們的SCHEDULER_NAME都是一樣的,這就導致不屬於自己的觸發器也查詢出來,然後根據trigger找到對應的JobDetail就出錯了,TRIGGER_STATE字段值變爲ERROR,觸發器執行失敗.

6.回到QuartzSchedulerThread類,我們知道這是一個執行觸發器的線程,這個線程因爲要時刻與數據庫交互,所以它應該是隨着應用啓動而存在的,我首先想到的是通過監聽器來實現,查看QuartzSchedulerThread引用位置,定位到QuartzScheduler這個類,看到這麼一段註釋:
在這裏插入圖片描述
7.這是Quartz的核心,它是Scheduler的一個間接實現,既然是核心,那麼一定有料,繼續追蹤引用,來到StdSchedulerFactory這個類,一個根據quartz.properties文件創建QuartzScheduler實例的工廠:
在這裏插入圖片描述
8.通過定位構造器的引用位置,最終來到了QuartzInitializerListener,隨着應用啓動創建了StdSchedulerFactory,從而實例化QuartzScheduler,通過QuartzScheduler實例化QuartzSchedulerThread,然後交由線程執行器管理執行;
在這裏插入圖片描述
在這裏插入圖片描述

驗證
步驟:
1.同時在本地啓動msmp和source-cp兩個項目,SCHEDULER_NAME都是默認值,quartz連的都是localhost→quartz,創建三個觸發器,這三個觸發器會在同一時刻被觸發:
在這裏插入圖片描述
2.觸發器執行之後,triggers表狀態如下:
在這裏插入圖片描述
3.說明只有第一個觸發器是被source-cp觸發,其他兩個都被msmp觸發導致觸發器執行錯誤;

4.給source-cp的quartz.properties文件添加一條配置 org.quartz.scheduler.instanceName=QuartzScheduler_SourceCp

5.同樣,創建三個觸發器,這三個觸發器會在同一時刻被觸發:
在這裏插入圖片描述
6…觸發器執行之後,triggers表狀態如下:
在這裏插入圖片描述
7.說明3個觸發器都被source-cp觸發,且都執行成功,再看源碼檢測的狀態也是檢測成功(綠色是沒加配置之前的檢測結果)!
在這裏插入圖片描述