基于Quartz開發(fā)企業(yè)級任務(wù)調(diào)度應(yīng)用
簡介: Quartz 是 OpenSymphony 開源組織在任務(wù)調(diào)度領(lǐng)域的一個開源項目,完全基于 Java 實現(xiàn)。作為一個優(yōu)秀的開源調(diào)度框架,Quartz 具有功能強(qiáng)大,應(yīng)用靈活,易于集成的特點(diǎn)。本文剖析了 Quartz 框架內(nèi)部的基本實現(xiàn)原理,通過一些具體實例描述了應(yīng)用 Quartz 開發(fā)應(yīng)用程序的基本方法,并對企業(yè)應(yīng)用中常見的問題及解決方案進(jìn)行了討論。
Quartz 基本概念及原理
Quartz Scheduler 開源框架
Quartz 是 OpenSymphony 開源組織在任務(wù)調(diào)度領(lǐng)域的一個開源項目,完全基于 Java 實現(xiàn)。該項目于 2009 年被 Terracotta 收購,目前是 Terracotta 旗下的一個項目。讀者可以到 http://www.quartz-scheduler.org/站點(diǎn)下載 Quartz 的發(fā)布版本及其源代碼。筆者在產(chǎn)品開發(fā)中使用的是版本 1.8.4,因此本文內(nèi)容基于該版本。本文不僅介紹如何應(yīng)用 Quartz 進(jìn)行開發(fā),也對其內(nèi)部實現(xiàn)原理作一定講解。
作為一個優(yōu)秀的開源調(diào)度框架,Quartz 具有以下特點(diǎn):
強(qiáng)大的調(diào)度功能,例如支持豐富多樣的調(diào)度方法,可以滿足各種常規(guī)及特殊需求;
靈活的應(yīng)用方式,例如支持任務(wù)和調(diào)度的多種組合方式,支持調(diào)度數(shù)據(jù)的多種存儲方式;
分布式和集群能力,Terracotta 收購后在原來功能基礎(chǔ)上作了進(jìn)一步提升。本文暫不討論該部分內(nèi)容
另外,作為 Spring 默認(rèn)的調(diào)度框架,Quartz 很容易與 Spring 集成實現(xiàn)靈活可配置的調(diào)度功能。
下面是本文中用到的一些專用詞匯,在此聲明:
scheduler:任務(wù)調(diào)度器 trigger:觸發(fā)器,用于定義任務(wù)調(diào)度時間規(guī)則 job:任務(wù),即被調(diào)度的任務(wù) misfire:錯過的,指本來應(yīng)該被執(zhí)行但實際沒有被執(zhí)行的任務(wù)調(diào)度
Quartz 任務(wù)調(diào)度的基本實現(xiàn)原理
核心元素
Quartz 任務(wù)調(diào)度的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任務(wù)調(diào)度的元數(shù)據(jù), scheduler 是實際執(zhí)行調(diào)度的控制器。
在 Quartz 中,trigger 是用于定義調(diào)度時間的元素,即按照什么時間規(guī)則去執(zhí)行任務(wù)。Quartz 中主要提供了四種類型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。這四種 trigger 可以滿足企業(yè)應(yīng)用中的絕大部分需求。我們將在企業(yè)應(yīng)用一節(jié)中進(jìn)一步討論四種 trigger 的功能。
在 Quartz 中,job 用于表示被調(diào)度的任務(wù)。主要有兩種類型的 job:無狀態(tài)的(stateless)和有狀態(tài)的(stateful)。對于同一個 trigger 來說,有狀態(tài)的 job 不能被并行執(zhí)行,只有上一次觸發(fā)的任務(wù)被執(zhí)行完之后,才能觸發(fā)下一次執(zhí)行。Job 主要有兩種屬性:volatility 和 durability,其中 volatility 表示任務(wù)是否被持久化到數(shù)據(jù)庫存儲,而 durability 表示在沒有 trigger 關(guān)聯(lián)的時候任務(wù)是否被保留。兩者都是在值為 true 的時候任務(wù)被持久化或保留。一個 job 可以被多個 trigger 關(guān)聯(lián),但是一個 trigger 只能關(guān)聯(lián)一個 job。
在 Quartz 中, scheduler 由 scheduler 工廠創(chuàng)建:DirectSchedulerFactory 或者 StdSchedulerFactory。 第二種工廠 StdSchedulerFactory 使用較多,因為 DirectSchedulerFactory 使用起來不夠方便,需要作許多詳細(xì)的手工編碼設(shè)置。 Scheduler 主要有三種:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。本文以最常用的 StdScheduler 為例講解。這也是筆者在項目中所使用的 scheduler 類。
Quartz 核心元素之間的關(guān)系如下圖所示:
圖 1. Quartz 核心元素關(guān)系圖
線程視圖
在 Quartz 中,有兩類線程,Scheduler 調(diào)度線程和任務(wù)執(zhí)行線程,其中任務(wù)執(zhí)行線程通常使用一個線程池維護(hù)一組線程。
圖 2. Quartz 線程視圖
Scheduler 調(diào)度線程主要有兩個: 執(zhí)行常規(guī)調(diào)度的線程,和執(zhí)行 misfired trigger 的線程。常規(guī)調(diào)度線程輪詢存儲的所有 trigger,如果有需要觸發(fā)的 trigger,即到達(dá)了下一次觸發(fā)的時間,則從任務(wù)執(zhí)行線程池獲取一個空閑線程,執(zhí)行與該 trigger 關(guān)聯(lián)的任務(wù)。Misfire 線程是掃描所有的 trigger,查看是否有 misfired trigger,如果有的話根據(jù) misfire 的策略分別處理。下圖描述了這兩個線程的基本流程:
圖 3. Quartz 調(diào)度線程流程圖
關(guān)于 misfired trigger,我們在企業(yè)應(yīng)用一節(jié)中將進(jìn)一步描述。
#p#
數(shù)據(jù)存儲
Quartz 中的 trigger 和 job 需要存儲下來才能被使用。Quartz 中有兩種存儲方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是將 trigger 和 job 存儲在內(nèi)存中,而 JobStoreSupport 是基于 jdbc 將 trigger 和 job 存儲到數(shù)據(jù)庫中。RAMJobStore 的存取速度非??欤怯捎谄湓谙到y(tǒng)被停止后所有的數(shù)據(jù)都會丟失,所以在通常應(yīng)用中,都是使用 JobStoreSupport。
在 Quartz 中,JobStoreSupport 使用一個驅(qū)動代理來操作 trigger 和 job 的數(shù)據(jù)存儲:StdJDBCDelegate。StdJDBCDelegate 實現(xiàn)了大部分基于標(biāo)準(zhǔn) JDBC 的功能接口,但是對于各種數(shù)據(jù)庫來說,需要根據(jù)其具體實現(xiàn)的特點(diǎn)做某些特殊處理,因此各種數(shù)據(jù)庫需要擴(kuò)展 StdJDBCDelegate 以實現(xiàn)這些特殊處理。Quartz 已經(jīng)自帶了一些數(shù)據(jù)庫的擴(kuò)展實現(xiàn),可以直接使用,如下圖所示:
圖 4. Quartz 數(shù)據(jù)庫驅(qū)動代理
作為嵌入式數(shù)據(jù)庫的代表,Derby 近來非常流行。如果使用 Derby 數(shù)據(jù)庫,可以使用上圖中的 CloudscapeDelegate 作為 trigger 和 job 數(shù)據(jù)存儲的代理類。
基本開發(fā)流程及簡單實例
搭建開發(fā)環(huán)境
利用 Quartz 進(jìn)行開發(fā)相當(dāng)簡單,只需要將下載開發(fā)包中的 quartz-all-1.8.4.jar 加入到 classpath 即可。根據(jù)筆者的經(jīng)驗,對于任務(wù)調(diào)度功能比較復(fù)雜的企業(yè)級應(yīng)用來說,***在開發(fā)階段將 Quartz 的源代碼導(dǎo)入到開發(fā)環(huán)境中來。一方面可以通過閱讀源碼了解 Quartz 的實現(xiàn)機(jī)理,另一方面可以通過擴(kuò)展或修改 Quartz 的一些類來實現(xiàn)某些 Quartz 尚不提供的功能。
圖 5. Quartz 實例工程及源碼導(dǎo)入
上圖中左邊是源碼導(dǎo)入后的截圖,其中 org.quartz.* 即為 quartz 的源碼。導(dǎo)入源碼后可能會有一些編譯錯誤,通常出現(xiàn)在 org.quartz.ee.* 和 org.quartz.jobs.ee.* 包中。下載開發(fā)包中有一個 lib 目錄,讀者可以將該目錄下的 jar 文件加入到編譯環(huán)境。如果還有編譯錯誤,讀者可以參考上圖中右側(cè)的 jar 列表,到網(wǎng)上去搜索下載。
項目中 com.ibm.zxn.sample.quartz 是我們自己的類包,下面的實例中我們會用到它。
一個簡單實例
Quartz 開發(fā)包中有一個 examples 目錄,其中有 15 個基本實例。建議讀者閱讀并實踐這些例子。本文這里只列舉一個小的實例,介紹基本的開發(fā)方法。
準(zhǔn)備數(shù)據(jù)庫和 Quartz 用的數(shù)據(jù)表
圖 6. Quartz 數(shù)據(jù)表
本文使用 IBM DB2 數(shù)據(jù)庫:將 jdbc 驅(qū)動程序 db2jcc.jar 加入到項目中;
在數(shù)據(jù)庫中創(chuàng)建一個新庫 QUARTZDB;
執(zhí)行 /quartz-1.8.4/docs/dbTables/tables_db2_v8.sql,創(chuàng)建數(shù)據(jù)表;表建好后如下所示:
準(zhǔn)備配置文件,加入到項目中
圖 7. 實例配置文件
通過實現(xiàn) job 接口定義我們自己的任務(wù)類,如下所示:
圖 8. 定義任務(wù)類
然后,實現(xiàn)任務(wù)調(diào)度的主程序,如下所示:
本實例中,我們利用 DateIntervalTrigger 實現(xiàn)一個每兩分鐘執(zhí)行一次的任務(wù)調(diào)度。
圖 9. 實現(xiàn)主程序
完成后項目結(jié)構(gòu)如下所示:
圖 10. 實例項目結(jié)構(gòu)圖
運(yùn)行程序,查看數(shù)據(jù)庫表和運(yùn)行結(jié)果
數(shù)據(jù)庫中,QRTZ_TRIGGERS 表中添加了一條 trigger 記錄,如下所示:
圖 11. QRTZ_TRIGGERS 表中的記錄
QRTZ_JOB_DETAILS 表中添加了一條 job 記錄,如下所示:
圖 12. QRTZ_JOB_DETAILES 表中的記錄
從運(yùn)行結(jié)果來看,任務(wù)每兩分鐘被執(zhí)行一次:
圖 13. 運(yùn)行結(jié)果
#p#
企業(yè)級開發(fā)中的常見應(yīng)用
在應(yīng)用 Quartz 進(jìn)行企業(yè)級的開發(fā)時,有一些問題會經(jīng)常遇到。本節(jié)筆者根據(jù)自己在項目開發(fā)中的經(jīng)驗,介紹企業(yè)開發(fā)中常見的一些問題以及通常的解決辦法。
應(yīng)用一:如何使用不同類型的 Trigger
前面我們提到 Quartz 中四種類型的 Trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger, 和 NthIncludedDayTrigger。
SimpleTrigger 一般用于實現(xiàn)每隔一定時間執(zhí)行任務(wù),以及重復(fù)多少次,如每 2 小時執(zhí)行一次,重復(fù)執(zhí)行 5 次。SimpleTrigger 內(nèi)部實現(xiàn)機(jī)制是通過計算間隔時間來計算下次的執(zhí)行時間,這就導(dǎo)致其不適合調(diào)度定時的任務(wù)。例如我們想每天的 1:00AM 執(zhí)行任務(wù),如果使用 SimpleTrigger 的話間隔時間就是一天。注意這里就會有一個問題,即當(dāng)有 misfired 的任務(wù)并且恢復(fù)執(zhí)行時,該執(zhí)行時間是隨機(jī)的(取決于何時執(zhí)行 misfired 的任務(wù),例如某天的 3:00PM)。這會導(dǎo)致之后每天的執(zhí)行時間都會變成 3:00PM,而不是我們原來期望的 1:00AM。
CronTirgger 類似于 LINUX 上的任務(wù)調(diào)度命令 crontab,即利用一個包含 7 個字段的表達(dá)式來表示時間調(diào)度方式。例如,"0 15 10 * * ? *" 表示每天的 10:1***M 執(zhí)行任務(wù)。對于涉及到星期和月份的調(diào)度,CronTirgger 是最適合的,甚至某些情況下是唯一選擇。例如,"0 10 14 ? 3 WED" 表示三月份的每個星期三的下午 14:10PM 執(zhí)行任務(wù)。讀者可以在具體用到該 trigger 時再詳細(xì)了解每個字段的含義。
DateIntervalTrigger 是 Quartz 1.7 之后的版本加入的,其最適合調(diào)度類似每 N(1, 2, 3...)小時,每 N 天,每 N 周等的任務(wù)。雖然 SimpleTrigger 也能實現(xiàn)類似的任務(wù),但是 DateIntervalTrigger 不會受到我們上面說到的 misfired 任務(wù)的影響。另外,DateIntervalTrigger 也不會受到 DST(Daylight Saving Time, 即中國的夏令時)調(diào)整的影響。筆者就曾經(jīng)因為該原因?qū)㈨椖恐械?SimpleTrigger 改為了 DateIntervalTrigger,因為如果使用 SimpleTrigger,本來設(shè)定的調(diào)度時間就會由于 DST 的調(diào)整而提前或延遲一個小時,而 DateIntervalTrigger 不會受此影響。
NthIncludedDayTrigger 的用途比較簡單明確,即用于每隔一個周期的第幾天調(diào)度任務(wù),例如,每個月的第 3 天執(zhí)行指定的任務(wù)。
除了上面提到的 4 種 Trigger,Quartz 中還定義了一個 Calendar 類(注意,是 org.quartz.Calendar)。這個 Calendar 與 Trigger 一起使用,但是它們的作用相反,它是用于排除任務(wù)不被執(zhí)行的情況。例如,按照 Trigger 的規(guī)則在 10 月 1 號需要執(zhí)行任務(wù),但是 Calendar 指定了 10 月 1 號是節(jié)日(國慶),所以任務(wù)在這一天將不會被執(zhí)行。通常來說,Calendar 用于排除節(jié)假日的任務(wù)調(diào)度,從而使任務(wù)只在工作日執(zhí)行。
應(yīng)用二:使用有狀態(tài)(StatefulJob)還是無狀態(tài)的任務(wù)(Job)
在 Quartz 中,Job 是一個接口,企業(yè)應(yīng)用需要實現(xiàn)這個接口以定義自己的任務(wù)。基本來說,任務(wù)分為有狀態(tài)和無狀態(tài)兩種。實現(xiàn) Job 接口的任務(wù)缺省為無狀態(tài)的。Quartz 中還有另外一個接口 StatefulJob。實現(xiàn) StatefulJob 接口的任務(wù)為有狀態(tài)的,上一節(jié)的簡單實例中,我們定義的 SampleJob 就是實現(xiàn)了 StatefulJob 接口的有狀態(tài)任務(wù)。下圖列出了 Quartz 中 Job 接口的定義以及一些自帶的實現(xiàn)類:
圖 14. Quartz 中 Job 接口定義
無狀態(tài)任務(wù)一般指可以并發(fā)的任務(wù),即任務(wù)之間是獨(dú)立的,不會互相干擾。例如我們定義一個 trigger,每 2 分鐘執(zhí)行一次,但是某些情況下一個任務(wù)可能需要 3 分鐘才能執(zhí)行完,這樣,在上一個任務(wù)還處在執(zhí)行狀態(tài)時,下一次觸發(fā)時間已經(jīng)到了。對于無狀態(tài)任務(wù),只要觸發(fā)時間到了就會被執(zhí)行,因為幾個相同任務(wù)可以并發(fā) 執(zhí)行。但是對有狀態(tài)任務(wù)來說,是不能并發(fā)執(zhí)行的,同一時間只能有一個任務(wù)在執(zhí)行。
在筆者項目中,某些任務(wù)需要對數(shù)據(jù)庫中的數(shù)據(jù)進(jìn)行增刪改處理。這些任務(wù)不能并發(fā)執(zhí)行,否則會造成數(shù)據(jù)混亂。因此我們使用 StatefulJob 接口?,F(xiàn)在回到上面的例子,任務(wù)每 2 分鐘執(zhí)行一次,若某次任務(wù)執(zhí)行了 5 分鐘才完成,Quartz 會怎么處理呢?按照 trigger 的規(guī)則,第 2 分鐘和第 4 分鐘分別會有一次預(yù)定的觸發(fā)執(zhí)行,但是由于是有狀態(tài)任務(wù),因此實際不會被觸發(fā)。在第 5 分鐘***次任務(wù)執(zhí)行完畢時,Quartz 會把第 2 和第 4 分鐘的兩次觸發(fā)作為 misfired job 進(jìn)行處理。對于 misfired job,Quartz 會查看其 misfire 策略是如何設(shè)定的,如果是立刻執(zhí)行,則會馬上啟動一次執(zhí)行,如果是等待下次執(zhí)行,則會忽略錯過的任務(wù),而等待下次(即第 6 分鐘)觸發(fā)執(zhí)行。
讀者可以在自己的項目中體會兩種任務(wù)的區(qū)別以及 Quartz 的處理方法,根據(jù)具體情況選擇不同類型的任務(wù)。
應(yīng)用三:如何設(shè)置 Quartz 的線程池和并發(fā)任務(wù)
Quartz 中自帶了一個線程池的實現(xiàn):SimpleThreadPool。類如其名,這只是線程池的一個簡單實現(xiàn),沒有提供動態(tài)自發(fā)調(diào)整等高級特性。Quartz 提供了一個配置參數(shù):org.quartz.threadPool.threadCount,可以在初始化時設(shè)定線程池的線程數(shù)量,但是一次設(shè)定后不能再 修改。假定這個數(shù)目是 10,則在并發(fā)任務(wù)達(dá)到 10 個以后,再有觸發(fā)的任務(wù)就無法被執(zhí)行了,只能等待有空閑線程的時候才能得到執(zhí)行。因此有些 trigger 就可能被 misfire。但是必須指出一點(diǎn),這個初始線程數(shù)并不是越大越好。當(dāng)并發(fā)線程太多時,系統(tǒng)整體性能反而會下降,因為系統(tǒng)把很多時間花在了線程調(diào)度上。根 據(jù)一般經(jīng)驗,這個值在 10 -- 50 比較合適。
對于一些注重性能的線程池來說,會根據(jù)實際線程使用情況進(jìn)行動態(tài)調(diào)整,例如初始線程數(shù),***線程數(shù),空閑線程數(shù)等。讀者在應(yīng)用中,如果有更好 的線程池,則可以在配置文件中通過下面參數(shù)替換 SimpleThreadPool:org.quartz.threadPool.class = myapp.GreatThreadPool。
應(yīng)用四:如何處理 Misfired 任務(wù)
在 Quartz 應(yīng)用中,misfired job 是經(jīng)常遇到的情況。一般來說,下面這些原因可能造成 misfired job:
1)系統(tǒng)因為某些原因被重啟。在系統(tǒng)關(guān)閉到重新啟動之間的一段時間里,可能有些任務(wù)會
被 misfire;
2)Trigger 被暫停(suspend)的一段時間里,有些任務(wù)可能會被 misfire;
3)線程池中所有線程都被占用,導(dǎo)致任務(wù)無法被觸發(fā)執(zhí)行,造成 misfire;
4)有狀態(tài)任務(wù)在下次觸發(fā)時間到達(dá)時,上次執(zhí)行還沒有結(jié)束;
為了處理 misfired job,Quartz 中為 trigger 定義了處理策略,主要有下面兩種:
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:針對 misfired job 馬上執(zhí)行一次;
MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次觸發(fā);
建議讀者在應(yīng)用開發(fā)中,將該設(shè)置作為可配置選項,使得用戶可以在使用過程中,針對已經(jīng)添加的 tirgger 動態(tài)配置該選項。
應(yīng)用五:如何保留已經(jīng)結(jié)束的 Trigger
在 Quartz 中,一個 tirgger 在***一次觸發(fā)完成之后,會被自動刪除。Quartz 默認(rèn)不會保留已經(jīng)結(jié)束的 trigger,如下面 Quartz 源代碼所示:
圖 15. executionComplete( ) 源碼
但是在實際應(yīng)用中,有些用戶需要保留以前的 trigger,作為歷史記錄,或者作為以后創(chuàng)建其他 trigger 的依據(jù)。如何保留結(jié)束的 trigger 呢?
一個辦法是應(yīng)用開發(fā)者自己維護(hù)一份數(shù)據(jù)備份記錄,并且與 Quartz 原表的記錄保持一定的同步。這個辦法實際操作起來比較繁瑣,而且容易出錯,不推薦使用。
另外一個辦法是通過修改并重新編譯 Quartz 的 trigger 類,修改其默認(rèn)的行為。我們以 org.quartz.SimpleTrigger 為例,修改上面代碼中 if (!mayFireAgain()) 部分的代碼如下:
圖 16. 修改 executionComplete( ) 源碼
另外我們需要在 SimpleTrigger 中定義一個新的類屬性:needRetain,如下所示:
圖 17. 定義新屬性 needRetain
在定義自己的 trigger 時,設(shè)置該屬性,就可以選擇是否在 trigger 結(jié)束時刪除 trigger。如下代碼所示:
圖 18. 使用修改后的 SimpleTrigger
有人可能會考慮通過定義一個新的類,然后繼承 org.quartz.SimpleTrigger 類并覆蓋 executionComplete( ) 方法來實現(xiàn)。但是這種方法是行不通的,因為 Quartz 內(nèi)部在處理時會根據(jù) trigger 的類型重新生成 SimpleTrigger 類的實例,而不是使用我們自己定義的類創(chuàng)建的實例。這一點(diǎn)應(yīng)該是 Quartz 的一個小小的不足之處,因為它把擴(kuò)展 trigger 的能力堵死了。好在 Quartz 是開源的,我們可以根據(jù)需要進(jìn)行修改。
小結(jié)
作為當(dāng)前頗具生命力的開源框架,Quartz 已經(jīng)得到了廣泛的應(yīng)用。Quartz 的強(qiáng)大功能和應(yīng)用靈活性,在企業(yè)應(yīng)用中發(fā)揮了巨大的作用。本文描述了如何應(yīng)用 Quartz 開發(fā)應(yīng)用程序,并對企業(yè)應(yīng)用中常見的問題及解決方案進(jìn)行了討論。
原文鏈接:IBM developerWorks