天天看點

基于 Quartz 開發企業級任務排程應用

原文位址:https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/

Quartz 是 OpenSymphony 開源組織在任務排程領域的一個開源項目,完全基于 Java 實作。作為一個優秀的開源排程架構,Quartz 具有功能強大,應用靈活,易于內建的特點。本文剖析了 Quartz 架構内部的基本實作原理,通過一些具體執行個體描述了應用 Quartz 開發應用程式的基本方法,并對企業應用中常見的問題及解決方案進行了讨論。

作為一個優秀的開源排程架構,Quartz 具有以下特點:

強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種正常及特殊需求;

靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種存儲方式;

分布式和叢集能力,Terracotta 收購後在原來功能基礎上作了進一步提升。本文暫不讨論該部分内容

另外,作為 Spring 預設的排程架構,Quartz 很容易與 Spring 內建實作靈活可配置的排程功能。

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

<dl></dl>

<dt>scheduler:</dt>

<dd>任務排程器</dd>

<dt>trigger:</dt>

<dd>觸發器,用于定義任務排程時間規則</dd>

<dt>job:</dt>

<dd>任務,即被排程的任務</dd>

<dt>misfire:</dt>

<dd>錯過的,指本來應該被執行但實際沒有被執行的任務排程</dd>

核心元素

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 開發企業級任務排程應用

線程視圖

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

基于 Quartz 開發企業級任務排程應用

Scheduler 排程線程主要有兩個: 執行正常排程的線程,和執行 misfired trigger 的線程。正常排程線程輪詢存儲的所有 trigger,如果有需要觸發的 trigger,即到達了下一次觸發的時間,則從任務執行線程池擷取一個空閑線程,執行與該 trigger 關聯的任務。Misfire 線程是掃描所有的 trigger,檢視是否有 misfired trigger,如果有的話根據 misfire 的政策分别處理。下圖描述了這兩個線程的基本流程:

基于 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 開發企業級任務排程應用

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

<a href="https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/#ibm-pcon">回頁首</a>

搭建開發環境

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

基于 Quartz 開發企業級任務排程應用

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

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

一個簡單執行個體

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

準備資料庫和 Quartz 用的資料表

本文使用 IBM DB2 資料庫:将 jdbc 驅動程式 db2jcc.jar 加入到項目中;

在資料庫中建立一個新庫 QUARTZDB;

執行 /quartz-1.8.4/docs/dbTables/tables_db2_v8.sql,建立資料表;表建好後如下所示:

基于 Quartz 開發企業級任務排程應用

準備配置檔案,加入到項目中

基于 Quartz 開發企業級任務排程應用

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

基于 Quartz 開發企業級任務排程應用

然後,實作任務排程的主程式,如下所示:

本執行個體中,我們利用 DateIntervalTrigger 實作一個每兩分鐘執行一次的任務排程。

基于 Quartz 開發企業級任務排程應用

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

基于 Quartz 開發企業級任務排程應用

運作程式,檢視資料庫表和運作結果

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

基于 Quartz 開發企業級任務排程應用

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

基于 Quartz 開發企業級任務排程應用

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

基于 Quartz 開發企業級任務排程應用

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

前面我們提到 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 用于排除節假日的任務排程,進而使任務隻在工作日執行。

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

基于 Quartz 開發企業級任務排程應用

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

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

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

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

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

在 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 動态配置該選項。

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

基于 Quartz 開發企業級任務排程應用

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

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

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

基于 Quartz 開發企業級任務排程應用

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

基于 Quartz 開發企業級任務排程應用

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

基于 Quartz 開發企業級任務排程應用

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

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