基於 Quartz 開發企業級任務調度應用

Quartz 基本概念及原理

Quartz Scheduler 開源框架

Quartz 是 OpenSymphony 開源組織在任務調度領域的一個開源項目,完全基於 Java 實現。該項目於 2009 年被 Terracotta 收購,目前是 Terracotta 旗下的一個項目。讀者可以到 http://www.quartz-scheduler.org/站點下載 Quartz 的發佈版本及其源代碼。筆者在產品開發中使用的是版本 1.8.4,因此本文內容基於該版本。本文不僅介紹如何應用 Quartz 進行開發,也對其內部實現原理作一定講解。

作為一個優秀的開源調度框架,Quartz 具有以下特點:

  1. 強大的調度功能,例如支持豐富多樣的調度方法,可以滿足各種常規及特殊需求;
  2. 靈活的應用方式,例如支持任務和調度的多種組合方式,支持調度數據的多種存儲方式;
  3. 分佈式和集群能力,Terracotta 收購後在原來功能基礎上作了進一步提升。本文暫不討論該部分內容

另外,作為 Spring 默認的調度框架,Quartz 很容易與 Spring 集成實現靈活可配置的調度功能。

下面是本文中用到的一些專用詞彙,在此聲明:

scheduler:

任務調度器

trigger:

觸發器,用於定義任務調度時間規則

job:

任務,即被調度的任務

misfire:

錯過的,指本來應該被執行但實際沒有被執行的任務調度

Quartz 任務調度的基本實現原理

核心元素

Quartz 任務調度的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任務調度的元數據, scheduler 是實際執行調度的控制器。

在 Quartz 中,trigger 是用於定義調度時間的元素,即按照什麼時間規則去執行任務。Quartz 中主要提供了四種類型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。這四種 trigger 可以滿足企業應用中的絕大部分需求。我們將在企業應用一節中進一步討論四種 trigger 的功能。

在 Quartz 中,job 用於表示被調度的任務。主要有兩種類型的 job:無狀態的(stateless)和有狀態的(stateful)。對於同一個 trigger 來說,有狀態的 job 不能被並行執行,只有上一次觸發的任務被執行完之後,才能觸發下一次執行。Job 主要有兩種屬性:volatility 和 durability,其中 volatility 表示任務是否被持久化到數據庫存儲,而 durability 表示在沒有 trigger 關聯的時候任務是否被保留。兩者都是在值為 true 的時候任務被持久化或保留。一個 job 可以被多個 trigger 關聯,但是一個 trigger 只能關聯一個 job。

在 Quartz 中, scheduler 由 scheduler 工廠創建:DirectSchedulerFactory 或者 StdSchedulerFactory。 第二種工廠 StdSchedulerFactory 使用較多,因為 DirectSchedulerFactory 使用起來不夠方便,需要作許多詳細的手工編碼設置。 Scheduler 主要有三種:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。本文以最常用的 StdScheduler 為例講解。這也是筆者在項目中所使用的 scheduler 類。

Quartz 核心元素之間的關係如下圖所示:

基於 Quartz 開發企業級任務調度應用

圖 1. Quartz 核心元素關係圖

線程視圖

在 Quartz 中,有兩類線程,Scheduler 調度線程和任務執行線程,其中任務執行線程通常使用一個線程池維護一組線程。

基於 Quartz 開發企業級任務調度應用

圖 2. Quartz 線程視圖

Scheduler 調度線程主要有兩個: 執行常規調度的線程,和執行 misfired trigger 的線程。常規調度線程輪詢存儲的所有 trigger,如果有需要觸發的 trigger,即到達了下一次觸發的時間,則從任務執行線程池獲取一個空閒線程,執行與該 trigger 關聯的任務。Misfire 線程是掃描所有的 trigger,查看是否有 misfired trigger,如果有的話根據 misfire 的策略分別處理。下圖描述了這兩個線程的基本流程:

基於 Quartz 開發企業級任務調度應用

圖 3. Quartz 調度線程流程圖

關於 misfired trigger,我們在企業應用一節中將進一步描述。

數據存儲

Quartz 中的 trigger 和 job 需要存儲下來才能被使用。Quartz 中有兩種存儲方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是將 trigger 和 job 存儲在內存中,而 JobStoreSupport 是基於 jdbc 將 trigger 和 job 存儲到數據庫中。RAMJobStore 的存取速度非常快,但是由於其在系統被停止後所有的數據都會丟失,所以在通常應用中,都是使用 JobStoreSupport。

在 Quartz 中,JobStoreSupport 使用一個驅動代理來操作 trigger 和 job 的數據存儲:StdJDBCDelegate。StdJDBCDelegate 實現了大部分基於標準 JDBC 的功能接口,但是對於各種數據庫來說,需要根據其具體實現的特點做某些特殊處理,因此各種數據庫需要擴展 StdJDBCDelegate 以實現這些特殊處理。Quartz 已經自帶了一些數據庫的擴展實現,可以直接使用,如下圖所示:

基於 Quartz 開發企業級任務調度應用

圖 4. Quartz 數據庫驅動代理

作為嵌入式數據庫的代表,Derby 近來非常流行。如果使用 Derby 數據庫,可以使用上圖中的 CloudscapeDelegate 作為 trigger 和 job 數據存儲的代理類。

基本開發流程及簡單實例

搭建開發環境

利用 Quartz 進行開發相當簡單,只需要將下載開發包中的 quartz-all-1.8.4.jar 加入到 classpath 即可。根據筆者的經驗,對於任務調度功能比較複雜的企業級應用來說,最好在開發階段將 Quartz 的源代碼導入到開發環境中來。一方面可以通過閱讀源碼瞭解 Quartz 的實現機理,另一方面可以通過擴展或修改 Quartz 的一些類來實現某些 Quartz 尚不提供的功能。

基於 Quartz 開發企業級任務調度應用

圖 5. Quartz 實例工程及源碼導入

上圖中左邊是源碼導入後的截圖,其中 org.quartz.* 即為 quartz 的源碼。導入源碼後可能會有一些編譯錯誤,通常出現在 org.quartz.ee.* 和 org.quartz.jobs.ee.* 包中。下載開發包中有一個 lib 目錄,讀者可以將該目錄下的 jar 文件加入到編譯環境。如果還有編譯錯誤,讀者可以參考上圖中右側的 jar 列表,到網上去搜索下載。

項目中 com.ibm.zxn.sample.quartz 是我們自己的類包,下面的實例中我們會用到它。

一個簡單實例

Quartz 開發包中有一個 examples 目錄,其中有 15 個基本實例。建議讀者閱讀並實踐這些例子。本文這裡只列舉一個小的實例,介紹基本的開發方法。

  1. 準備數據庫和 Quartz 用的數據表
  2. 本文使用 IBM DB2 數據庫:將 jdbc 驅動程序 db2jcc.jar 加入到項目中;
  3. 在數據庫中創建一個新庫 QUARTZDB;
  4. 執行 /quartz-1.8.4/docs/dbTables/tables_db2_v8.sql,創建數據表;表建好後如下所示:
基於 Quartz 開發企業級任務調度應用

圖 6. Quartz 數據表

2. 準備配置文件,加入到項目中

基於 Quartz 開發企業級任務調度應用

圖 7. 實例配置文件

3. 通過實現 job 接口定義我們自己的任務類,如下所示:

基於 Quartz 開發企業級任務調度應用

圖 8. 定義任務類

4. 然後,實現任務調度的主程序,如下所示:

本實例中,我們利用 DateIntervalTrigger 實現一個每兩分鐘執行一次的任務調度。

基於 Quartz 開發企業級任務調度應用

圖 9. 實現主程序

5. 完成後項目結構如下所示:

基於 Quartz 開發企業級任務調度應用

圖 10. 實例項目結構圖

6. 運行程序,查看數據庫表和運行結果

數據庫中,QRTZ_TRIGGERS 表中添加了一條 trigger 記錄,如下所示:

基於 Quartz 開發企業級任務調度應用

圖 11. QRTZ_TRIGGERS 表中的記錄

QRTZ_JOB_DETAILS 表中添加了一條 job 記錄,如下所示:

基於 Quartz 開發企業級任務調度應用

圖 12. QRTZ_JOB_DETAILES 表中的記錄

從運行結果來看,任務每兩分鐘被執行一次:

基於 Quartz 開發企業級任務調度應用

圖 13. 運行結果

企業級開發中的常見應用

在應用 Quartz 進行企業級的開發時,有一些問題會經常遇到。本節筆者根據自己在項目開發中的經驗,介紹企業開發中常見的一些問題以及通常的解決辦法。

應用一:如何使用不同類型的 Trigger

前面我們提到 Quartz 中四種類型的 Trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger, 和 NthIncludedDayTrigger。

SimpleTrigger: 一般用於實現每隔一定時間執行任務,以及重複多少次,如每 2 小時執行一次,重複執行 5 次。SimpleTrigger 內部實現機制是通過計算間隔時間來計算下次的執行時間,這就導致其不適合調度定時的任務。例如我們想每天的 1:00AM 執行任務,如果使用 SimpleTrigger 的話間隔時間就是一天。注意這裡就會有一個問題,即當有 misfired 的任務並且恢復執行時,該執行時間是隨機的(取決於何時執行 misfired 的任務,例如某天的 3:00PM)。這會導致之後每天的執行時間都會變成 3:00PM,而不是我們原來期望的 1:00AM。

CronTirgger :

類似於 LINUX 上的任務調度命令 crontab,即利用一個包含 7 個字段的表達式來表示時間調度方式。例如,"0 15 10 * * ? *" 表示每天的 10:15AM 執行任務。對於涉及到星期和月份的調度,CronTirgger 是最適合的,甚至某些情況下是唯一選擇。例如,"0 10 14 ? 3 WED" 表示三月份的每個星期三的下午 14:10PM 執行任務。讀者可以在具體用到該 trigger 時再詳細瞭解每個字段的含義。

DateIntervalTrigger: 是 Quartz 1.7 之後的版本加入的,其最適合調度類似每 N(1, 2, 3...)小時,每 N 天,每 N 周等的任務。雖然 SimpleTrigger 也能實現類似的任務,但是 DateIntervalTrigger 不會受到我們上面說到的 misfired 任務的影響。另外,DateIntervalTrigger 也不會受到 DST(Daylight Saving Time, 即中國的夏令時)調整的影響。筆者就曾經因為該原因將項目中的 SimpleTrigger 改為了 DateIntervalTrigger,因為如果使用 SimpleTrigger,本來設定的調度時間就會由於 DST 的調整而提前或延遲一個小時,而 DateIntervalTrigger 不會受此影響。

NthIncludedDayTrigger :的用途比較簡單明確,即用於每隔一個週期的第幾天調度任務,例如,每個月的第 3 天執行指定的任務。

除了上面提到的 4 種 Trigger,Quartz 中還定義了一個 Calendar 類(注意,是 org.quartz.Calendar)。這個 Calendar 與 Trigger 一起使用,但是它們的作用相反,它是用於排除任務不被執行的情況。例如,按照 Trigger 的規則在 10 月 1 號需要執行任務,但是 Calendar 指定了 10 月 1 號是節日(國慶),所以任務在這一天將不會被執行。通常來說,Calendar 用於排除節假日的任務調度,從而使任務只在工作日執行。

應用二:使用有狀態(StatefulJob)還是無狀態的任務(Job)

在 Quartz 中,Job 是一個接口,企業應用需要實現這個接口以定義自己的任務。基本來說,任務分為有狀態和無狀態兩種。實現 Job 接口的任務缺省為無狀態的。Quartz 中還有另外一個接口 StatefulJob。實現 StatefulJob 接口的任務為有狀態的,上一節的簡單實例中,我們定義的 SampleJob 就是實現了 StatefulJob 接口的有狀態任務。下圖列出了 Quartz 中 Job 接口的定義以及一些自帶的實現類:

基於 Quartz 開發企業級任務調度應用

圖 14. Quartz 中 Job 接口定義

無狀態任務一般指可以併發的任務,即任務之間是獨立的,不會互相干擾。例如我們定義一個 trigger,每 2 分鐘執行一次,但是某些情況下一個任務可能需要 3 分鐘才能執行完,這樣,在上一個任務還處在執行狀態時,下一次觸發時間已經到了。對於無狀態任務,只要觸發時間到了就會被執行,因為幾個相同任務可以併發執行。但是對有狀態任務來說,是不能併發執行的,同一時間只能有一個任務在執行。

在筆者項目中,某些任務需要對數據庫中的數據進行增刪改處理。這些任務不能併發執行,否則會造成數據混亂。因此我們使用 StatefulJob 接口。現在回到上面的例子,任務每 2 分鐘執行一次,若某次任務執行了 5 分鐘才完成,Quartz 會怎麼處理呢?按照 trigger 的規則,第 2 分鐘和第 4 分鐘分別會有一次預定的觸發執行,但是由於是有狀態任務,因此實際不會被觸發。在第 5 分鐘第一次任務執行完畢時,Quartz 會把第 2 和第 4 分鐘的兩次觸發作為 misfired job 進行處理。對於 misfired job,Quartz 會查看其 misfire 策略是如何設定的,如果是立刻執行,則會馬上啟動一次執行,如果是等待下次執行,則會忽略錯過的任務,而等待下次(即第 6 分鐘)觸發執行。

讀者可以在自己的項目中體會兩種任務的區別以及 Quartz 的處理方法,根據具體情況選擇不同類型的任務。

應用三:如何設置 Quartz 的線程池和併發任務

Quartz 中自帶了一個線程池的實現:SimpleThreadPool。類如其名,這只是線程池的一個簡單實現,沒有提供動態自發調整等高級特性。Quartz 提供了一個配置參數:org.quartz.threadPool.threadCount,可以在初始化時設定線程池的線程數量,但是一次設定後不能再修改。假定這個數目是 10,則在併發任務達到 10 個以後,再有觸發的任務就無法被執行了,只能等待有空閒線程的時候才能得到執行。因此有些 trigger 就可能被 misfire。但是必須指出一點,這個初始線程數並不是越大越好。當併發線程太多時,系統整體性能反而會下降,因為系統把很多時間花在了線程調度上。根據一般經驗,這個值在 10 -- 50 比較合適。

對於一些注重性能的線程池來說,會根據實際線程使用情況進行動態調整,例如初始線程數,最大線程數,空閒線程數等。讀者在應用中,如果有更好的線程池,則可以在配置文件中通過下面參數替換 SimpleThreadPool:org.quartz.threadPool.class = myapp.GreatThreadPool。

應用四:如何處理 Misfired 任務

在 Quartz 應用中,misfired job 是經常遇到的情況。一般來說,下面這些原因可能造成 misfired job:

1)系統因為某些原因被重啟。在系統關閉到重新啟動之間的一段時間裡,可能有些任務會

被 misfire;

2)Trigger 被暫停(suspend)的一段時間裡,有些任務可能會被 misfire;

3)線程池中所有線程都被佔用,導致任務無法被觸發執行,造成 misfire;

4)有狀態任務在下次觸發時間到達時,上次執行還沒有結束;

為了處理 misfired job,Quartz 中為 trigger 定義了處理策略,主要有下面兩種:

MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:針對 misfired job 馬上執行一次;

MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次觸發;

建議讀者在應用開發中,將該設置作為可配置選項,使得用戶可以在使用過程中,針對已經添加的 tirgger 動態配置該選項。

應用五:如何保留已經結束的 Trigger

在 Quartz 中,一個 tirgger 在最後一次觸發完成之後,會被自動刪除。Quartz 默認不會保留已經結束的 trigger,如下面 Quartz 源代碼所示:

基於 Quartz 開發企業級任務調度應用

圖 15. executionComplete( ) 源碼

但是在實際應用中,有些用戶需要保留以前的 trigger,作為歷史記錄,或者作為以後創建其他 trigger 的依據。如何保留結束的 trigger 呢?

一個辦法是應用開發者自己維護一份數據備份記錄,並且與 Quartz 原表的記錄保持一定的同步。這個辦法實際操作起來比較繁瑣,而且容易出錯,不推薦使用。

另外一個辦法是通過修改並重新編譯 Quartz 的 trigger 類,修改其默認的行為。我們以 org.quartz.SimpleTrigger 為例,修改上面代碼中 if (!mayFireAgain()) 部分的代碼如下:

基於 Quartz 開發企業級任務調度應用

圖 16. 修改 executionComplete( ) 源碼

另外我們需要在 SimpleTrigger 中定義一個新的類屬性:needRetain,如下所示:

基於 Quartz 開發企業級任務調度應用

圖 17. 定義新屬性 needRetain

在定義自己的 trigger 時,設置該屬性,就可以選擇是否在 trigger 結束時刪除 trigger。如下代碼所示:

基於 Quartz 開發企業級任務調度應用

圖 18. 使用修改後的 SimpleTrigger

有人可能會考慮通過定義一個新的類,然後繼承 org.quartz.SimpleTrigger 類並覆蓋 executionComplete( ) 方法來實現。但是這種方法是行不通的,因為 Quartz 內部在處理時會根據 trigger 的類型重新生成 SimpleTrigger 類的實例,而不是使用我們自己定義的類創建的實例。這一點應該是 Quartz 的一個小小的不足之處,因為它把擴展 trigger 的能力堵死了。好在 Quartz 是開源的,我們可以根據需要進行修改。

小結

作為當前頗具生命力的開源框架,Quartz 已經得到了廣泛的應用。Quartz 的強大功能和應用靈活性,在企業應用中發揮了巨大的作用。本文描述瞭如何應用 Quartz 開發應用程序,並對企業應用中常見的問題及解決方案進行了討論。


分享到:


相關文章: