描述
強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種常規及特殊需求;
靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種儲存方式;
分散式和叢集能力,Terracotta 收購後在原來功能基礎上作了進一步提升。
說明:
scheduler:
任務排程器
trigger:
觸發器,用於定義任務排程時間規則
job:
任務,即被排程的任務
misfire:
錯過的,指本來應該被執行但實際沒有被執行的任務排程。
強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種常規及特殊需求; 靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種儲存方式; 分散式和叢集能力,Terracotta 收購後在原來功能基礎上作了進一步提升。另外,作為 Spring 預設的排程框架,Quartz 很容易與 Spring 整合實現靈活可配置的排程功能。
quartz元素介紹
Quartz 任務排程的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任務排程的元資料, scheduler 是實際執行排程的控制器。
trigger
在 Quartz 中,trigger 是用於定義排程時間的元素,即按照什麼時間規則去執行任務。Quartz 主要提供了四種類型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。這四種類型 trigger 可以滿足企業應用中的絕大部分需求。我們將在企業應用一節中進一步討論四種類型 trigger 的功能。
job
在 Quartz 中,job 用於表示被排程的任務。主要有兩種型別的 job:無狀態的(stateless)和有狀態的(stateful)。對於同一個 trigger 來說,有狀態的 job 不能被並行執行,只有上一次觸發的任務被執行完之後,才能觸發下一次執行。Job 主要有兩種屬性:volatility 和 durability,其中 volatility 表示任務是否被持久化到資料庫儲存,而 durability 表示在沒有 trigger 關聯的時候任務是否被保留。兩者都是在值為 true 的時候任務被持久化或保留。一個 job 可以被多個 trigger 關聯,但是一個 trigger 只能關聯一個 job。
scheduler
在 Quartz 中, scheduler 由 scheduler 工廠建立:DirectSchedulerFactory 或者 StdSchedulerFactory。
第二種工廠 StdSchedulerFactory 使用較多,因為 DirectSchedulerFactory 使用起來不夠方便,需要作許多詳細的手工編碼設定。
Scheduler 主要有三種:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。本文以最常用的 StdScheduler 為例講解。這也是筆者在專案中所使用的 scheduler 類。
Quartz 核心元素之間的關係如下圖所示:
Quartz 核心元素之間的關係
在 Quartz 中,有兩類執行緒,Scheduler 排程執行緒和任務執行執行緒,其中任務執行執行緒通常使用一個執行緒池維護一組執行緒。
圖 2. Quartz 執行緒檢視
Quartz 執行緒
Scheduler 排程執行緒主要有兩個: 執行常規排程的執行緒,和 執行 misfired trigger 的執行緒。
常規排程執行緒輪詢儲存的所有 trigger,如果有需要觸發的 trigger,即到達了下一次觸發的時間,則從任務執行執行緒池獲取一個空閒執行緒,執行與該 trigger 關聯的任務。Misfire 執行緒是掃描所有的 trigger,檢視是否有 misfired trigger,如果有的話根據 misfire 的策略分別處理。
資料儲存
Quartz 中的 trigger 和 job 需要儲存下來才能被使用。Quartz 中有兩種儲存方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是將 trigger 和 job 儲存在記憶體中,而 JobStoreSupport 是基於 jdbc 將 trigger 和 job 儲存到資料庫中。RAMJobStore 的存取速度非常快,但是由於其在系統被停止後所有的資料都會丟失,所以在通常應用中,都是使用 JobStoreSupport。
在 Quartz 中,JobStoreSupport 使用一個驅動代理來操作 trigger 和 job 的資料儲存:StdJDBCDelegate。StdJDBCDelegate 實現了大部分基於標準 JDBC 的功能介面,但是對於各種資料庫來說,需要根據其具體實現的特點做某些特殊處理,因此各種資料庫需要擴充套件 StdJDBCDelegate 以實現這些特殊處理。Quartz 已經自帶了一些資料庫的擴充套件實現,可以直接使用,如下圖所示:
StdJDBCDelegate
圖 4. Quartz 資料庫驅動代理
作為嵌入式資料庫的代表,Derby 近來非常流行。如果使用 Derby 資料庫,可以使用上圖中的 CloudscapeDelegate 作為 trigger 和 job 資料儲存的代理類。
企業級開發中的常見應用 在應用 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 執行任務。
cron表示式語法: [秒] [分] [小時] [日] [月] [周] [年]
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 介面的任務為有狀態的,下圖列出了 Quartz 中 Job 介面的定義以及一些自帶的實現類:
圖 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 動態配置該選項。
表名稱 |
說明 |
qrtz_blob_triggers |
Trigger作為Blob型別儲存(用於Quartz使用者用JDBC建立他們自己定製的Trigger型別,JobStore 並不知道如何儲存例項的時候) |
qrtz_calendars |
以Blob型別儲存Quartz的Calendar日曆資訊, quartz可配置一個日曆來指定一個時間範圍 |
qrtz_cron_triggers |
儲存Cron Trigger,包括Cron表示式和時區資訊。 |
qrtz_fired_triggers |
儲存與已觸發的Trigger相關的狀態資訊,以及相聯Job的執行資訊 |
qrtz_job_details | 儲存每一個已配置的Job的詳細資訊 |
qrtz_locks |
儲存程式的悲觀鎖的資訊(假如使用了悲觀鎖) |
qrtz_paused_trigger_graps |
儲存已暫停的Trigger組的資訊 |
qrtz_scheduler_state |
儲存少量的有關 Scheduler的狀態資訊,和別的 Scheduler 例項(假如是用於一個叢集中) |
qrtz_simple_triggers |
儲存簡單的 Trigger,包括重複次數,間隔,以及已觸的次數 |
qrtz_triggers |
儲存已配置的 Trigger的資訊 |
qrzt_simprop_triggers |
核心類解釋說明
(1)核心類 QuartzSchedulerThread:負責執行向QuartzScheduler註冊的觸發Trigger的工作的執行緒。 ThreadPool:Scheduler使用一個執行緒池作為任務執行的基礎設施,任務透過共享執行緒池中的執行緒提供執行效率。 QuartzSchedulerResources:包含建立QuartzScheduler例項所需的所有資源(JobStore,ThreadPool等)。 SchedulerFactory :提供用於獲取排程程式例項的客戶端可用控制代碼的機制。 JobStore: 透過類實現的介面,這些類要為org.quartz.core.QuartzScheduler的使用提供一個org.quartz.Job和org.quartz.Trigger儲存機制。作業和觸發器的儲存應該以其名稱和組的組合為唯一性。 QuartzScheduler :這是Quartz的核心,它是org.quartz.Scheduler介面的間接實現,包含排程org.quartz.Jobs,註冊org.quartz.JobListener例項等的方法。 Scheduler :這是Quartz Scheduler的主要介面,代表一個獨立執行容器。排程程式維護JobDetails和觸發器的登錄檔。 一旦註冊,排程程式負責執行作業,當他們的相關聯的觸發器觸發(當他們的預定時間到達時)。 Trigger :具有所有觸發器通用屬性的基本介面,描述了job執行的時間觸發規則。 - 使用TriggerBuilder例項化實際觸發器。 JobDetail :傳遞給定作業例項的詳細資訊屬性。 JobDetails將使用JobBuilder建立/定義。 Job:要由表示要執行的“作業”的類實現的介面。只有一個方法 void execute(jobExecutionContext context) (jobExecutionContext 提供排程上下文各種資訊,執行時資料儲存在jobDataMap中) Job有個子介面StatefulJob ,代表有狀態任務。有狀態任務不可併發,前次任務沒有執行完,後面任務處於阻塞等待。
其他知識點
quartz 實際並不關心你是在相同的還是不同的機器上執行節點。 當叢集是放置在不同的機器上時,通常稱之為水平叢集。節點是跑在同一臺機器是,稱之為垂直叢集。
配置屬性介紹
org.quartz.scheduler.instanceName: 屬性可為任何值,用在 JDBC JobStore 中來唯一標識例項,但是所有叢集節點中必須相同。
org.quartz.scheduler.instanceId: 屬性為 AUTO即可,基於主機名和時間戳來產生例項 ID。
org.quartz.jobStore.class: 屬性為 JobStoreTX,將任務持久化到資料中。因為叢集中節點依賴於資料庫來傳播 Scheduler 例項的狀態,你只能在使用 JDBC JobStore 的應用 Quartz 叢集。 這意味著你必須使用 JobStoreTX 或是 JobStoreCMT 作為 Job 儲存;你不能在叢集中使用 RAMJobStore。
org.quartz.jobStore.isClustered: 屬性為 true, Scheduler 例項要它參與到一個叢集當中。 這一屬性會貫穿於排程框架的始終,用於修改叢集環境中操作的預設行為。
org.quartz.jobStore.clusterCheckinInterval: 屬性定義了Scheduler 例項檢入到資料庫中的頻率(單位:毫秒)。 Scheduler 檢查是否其他的例項到了它們應當檢入的時候未檢入;這能指出一個失敗的 Scheduler 例項,且當前 Scheduler 會以此來接管任何執行失敗並可恢復的 Job。 透過檢入操作,Scheduler 也會更新自身的狀態記錄。clusterChedkinInterval 越小,Scheduler 節點檢查失敗的 Scheduler 例項就越頻繁。預設值是 15000 (即15 秒)。