Quartz是OpenSymphony開源組織在任務排程領域的一個開源項目,完全基于Java實作。作為一個優秀的開源排程架構,Quartz具有以下特點:
(1)強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種正常及特殊需求;
(2)靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種存儲方式;
(3)分布式和叢集能力,Terracotta收購後在原來功能基礎上作了進一步提升。本文将對該部分相加闡述。
Quartz任務排程的核心元素為:Scheduler——任務排程器、Trigger——觸發器、Job——任務。其中trigger和job是任務排程的中繼資料,scheduler是實際執行排程的控制器。
Trigger是用于定義排程時間的元素,即按照什麼時間規則去執行任務。Quartz中主要提供了四種類型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。這四種trigger可以滿足企業應用中的絕大部分需求。
Job用于表示被排程的任務。主要有兩種類型的job:無狀态的(stateless)和有狀态的(stateful)。對于同一個trigger來說,有狀态的job不能被并行執行,隻有上一次觸發的任務被執行完之後,才能觸發下一次執行。Job主要有兩種屬性:volatility和durability,其中volatility表示任務是否被持久化到資料庫存儲,而durability表示在沒有trigger關聯的時候任務是否被保留。兩者都是在值為true的時候任務被持久化或保留。一個job可以被多個trigger關聯,但是一個trigger隻能關聯一個job。
Scheduler由scheduler工廠建立:DirectSchedulerFactory或者StdSchedulerFactory。第二種工廠StdSchedulerFactory使用較多,因為DirectSchedulerFactory使用起來不夠友善,需要作許多詳細的手工編碼設定。Scheduler主要有三種:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。
Quartz核心元素之間的關系如圖1.1所示:
圖1.1 核心元素關系圖
在Quartz中,有兩類線程,Scheduler排程線程和任務執行線程,其中任務執行線程通常使用一個線程池維護一組線程。
圖1.2 Quartz線程視圖
Scheduler排程線程主要有兩個:執行正常排程的線程,和執行misfiredtrigger的線程。正常排程線程輪詢存儲的所有trigger,如果有需要觸發的trigger,即到達了下一次觸發的時間,則從任務執行線程池擷取一個空閑線程,執行與該trigger關聯的任務。Misfire線程是掃描所有的trigger,檢視是否有misfiredtrigger,如果有的話根據misfire的政策分别處理(fire now OR wait for the next fire)。
Quartz中的trigger和job需要存儲下來才能被使用。Quartz中有兩種存儲方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存儲在記憶體中,而JobStoreSupport是基于jdbc将trigger和job存儲到資料庫中。RAMJobStore的存取速度非常快,但是由于其在系統被停止後所有的資料都會丢失,是以在叢集應用中,必須使用JobStoreSupport。
一個Quartz叢集中的每個節點是一個獨立的Quartz應用,它又管理着其他的節點。這就意味着你必須對每個節點分别啟動或停止。Quartz叢集中,獨立的Quartz節點并不與另一其的節點或是管理節點通信,而是通過相同的資料庫表來感覺到另一Quartz應用的,如圖2.1所示。
圖2.1 Quartz叢集架構
因為Quartz叢集依賴于資料庫,是以必須首先建立Quartz資料庫表,Quartz釋出包中包括了所有被支援的資料庫平台的SQL腳本。這些SQL腳本存放于<quartz_home>/docs/dbTables 目錄下。這裡采用的Quartz 1.8.4版本,總共12張表,不同版本,表個數可能不同。資料庫為mysql,用tables_mysql.sql建立資料庫表。全部表如圖2.2所示,對這些表的簡要介紹如圖2.3所示。
圖2.2 Quartz 1.8.4在mysql資料庫中生成的表
圖2.3 Quartz資料表簡介
說明:叢集中節點執行個體資訊,Quartz定時讀取該表的資訊判斷叢集中每個執行個體的目前狀态。
instance_name:配置檔案中org.quartz.scheduler.instanceId配置的名字,如果設定為AUTO,quartz會根據實體機名和目前時間産生一個名字。
last_checkin_time:上次檢入時間
checkin_interval:檢入間隔時間
存儲與已觸發的Trigger相關的狀态資訊,以及相聯Job的執行資訊。
trigger_name:trigger的名字,該名字使用者自己可以随意定制,無強行要求
trigger_group:trigger所屬組的名字,該名字使用者自己随意定制,無強行要求
job_name:qrtz_job_details表job_name的外鍵
job_group:qrtz_job_details表job_group的外鍵
trigger_state:目前trigger狀态設定為ACQUIRED,如果設為WAITING,則job不會觸發
trigger_cron:觸發器類型,使用cron表達式
說明:儲存job詳細資訊,該表需要使用者根據實際情況初始化
job_name:叢集中job的名字,該名字使用者自己可以随意定制,無強行要求。
job_group:叢集中job的所屬組的名字,該名字使用者自己随意定制,無強行要求。
job_class_name:叢集中job實作類的完全包名,quartz就是根據這個路徑到classpath找到該job類的。
is_durable:是否持久化,把該屬性設定為1,quartz會把job持久化到資料庫中
job_data:一個blob字段,存放持久化job對象。
說明:tables_oracle.sql裡有相應的dml初始化,如圖2.4所示。
圖2.4 Quartz權限資訊表中的初始化資訊
Quartz Scheduler自身是察覺不到被叢集的,隻有配置給Scheduler的JDBC JobStore才知道。當Quartz Scheduler啟動時,它調用JobStore的schedulerStarted()方法,它告訴JobStore Scheduler已經啟動了。schedulerStarted() 方法是在JobStoreSupport類中實作的。JobStoreSupport類會根據quartz.properties檔案中的設定來确定Scheduler執行個體是否參與到叢集中。假如配置了叢集,一個新的ClusterManager類的執行個體就被建立、初始化并啟動。ClusterManager是在JobStoreSupport類中的一個内嵌類,繼承了java.lang.Thread,它會定期運作,并對Scheduler執行個體執行檢入的功能。Scheduler也要檢視是否有任何一個别的叢集節點失敗了。檢入操作執行周期在quartz.properties中配置。
當一個Scheduler執行個體執行檢入時,它會檢視是否有其他的Scheduler執行個體在到達他們所預期的時間還未檢入。這是通過檢查SCHEDULER_STATE表中Scheduler記錄在LAST_CHEDK_TIME列的值是否早于org.quartz.jobStore.clusterCheckinInterval來确定的。如果一個或多個節點到了預定時間還沒有檢入,那麼運作中的Scheduler就假定它(們) 失敗了。
當一個Sheduler執行個體在執行某個Job時失敗了,有可能由另一正常工作的Scheduler執行個體接過這個Job重新運作。要實作這種行為,配置給JobDetail對象的Job可恢複屬性必須設定為true(job.setRequestsRecovery(true))。如果可恢複屬性被設定為false(預設為false),當某個Scheduler在運作該job失敗時,它将不會重新運作;而是由另一個Scheduler執行個體在下一次觸發時間觸發。Scheduler執行個體出現故障後多快能被偵測到取決于每個Scheduler的檢入間隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。
Spring從2.0.2開始便不再支援Quartz。具體表現在 Quartz+Spring 把 Quartz 的 Task 執行個體化進入資料庫時,會産生: Serializable 的錯誤:
這個 MethodInvokingJobDetailFactoryBean 類中的 methodInvoking 方法,是不支援序列化的,是以在把 QUARTZ 的 TASK 序列化進入資料庫時就會抛錯。
首先解決MethodInvokingJobDetailFactoryBean的問題,在不修改Spring源碼的情況下,可以避免使用這個類,直接調用JobDetail。但是使用JobDetail實作,需要自己實作MothodInvoking的邏輯,可以使用JobDetail的jobClass和JobDataAsMap屬性來自定義一個Factory(Manager)來實作同樣的目的。例如,本示例中建立了一個MyDetailQuartzJobBean來實作這個功能。
在Test類中,隻是簡單實作了列印系統目前時間的功能。
ServerA、ServerB的代碼、配置完全一樣,先啟動ServerA,後啟動ServerB,當Server關斷之後,ServerB會監測到其關閉,并将ServerA上正在執行的Job接管,繼續執行。
盡管我們已經實作了Spring+Quartz的叢集配置,但是因為Spring與Quartz之間的相容問題還是不建議使用該方式。在本小節中,我們實作了單獨用Quartz配置的叢集,相對Spring+Quartz的方式來說,簡單、穩定。
我們采用單獨使用Quartz來實作其叢集功能,代碼結構及所需的第三方jar包如圖3.1所示。其中,Mysql版本:5.1.52,Mysql驅動版本:mysql-connector-java-5.1.5-bin.jar(針對于5.1.52,建議采用該版本驅動,因為Quartz存在BUG使得其與某些Mysql驅動結合時不能正常運作)。
圖4.1 Quartz叢集工程結構及所需第三方jar包
其中quartz.properties為Quartz配置檔案,放在src目錄下,若無該檔案,Quartz将自動加載jar包中的quartz.properties檔案;SimpleRecoveryJob.java、SimpleRecoveryStatefulJob.java為兩個Job;ClusterExample.java中編寫了排程資訊、觸發機制及相應的測試main函數。
預設檔案名稱quartz.properties,通過設定"org.quartz.jobStore.isClustered"屬性為"true"來激活叢集特性。在叢集中的每一個執行個體都必須有一個唯一的"instance id" ("org.quartz.scheduler.instanceId" 屬性), 但是應該有相同的"scheduler instance name" ("org.quartz.scheduler.instanceName"),也就是說叢集中的每一個執行個體都必須使用相同的quartz.properties 配置檔案。除了以下幾種例外,配置檔案的内容其他都必須相同:
a.線程池大小。
b.不同的"org.quartz.scheduler.instanceId"屬性值(通過設定為"AUTO"即可)。
Server A與Server B中的配置和代碼完全一樣。運作方法:運作任意主機上的ClusterExample.java,将任務加入排程,觀察運作結果:
運作ServerA,結果如圖4.2所示。
圖4.2 ServerA運作結果1
開啟ServerB後,ServerA與ServerB的輸出如圖4.3、4.4所示。
圖4.3 ServerA運作結果2
圖4.4 ServerB運作結果1
從圖4.3、4.4可以看出,ServerB開啟後,系統自動實作了負責均衡,ServerB接手Job1。關斷ServerA後,ServerB的運作結果如圖4.5所示。
圖4.5 ServerB運作結果2
從圖4.5中可以看出,ServerB可以檢測出ServerA丢失,将其負責的任務Job2接手,并将ServerA丢失到Server檢測出這段異常時間中需要執行的Job2重新執行了。
Quartz實際并不關心你是在相同還是不同的機器上運作節點。當叢集放置在不同的機器上時,稱之為水準叢集。節點跑在同一台機器上時,稱之為垂直叢集。對于垂直叢集,存在着單點故障的問題。這對高可用性的應用來說是無法接受的,因為一旦機器崩潰了,所有的節點也就被終止了。對于水準叢集,存在着時間同步問題。
節點用時間戳來通知其他執行個體它自己的最後檢入時間。假如節點的時鐘被設定為将來的時間,那麼運作中的Scheduler将再也意識不到那個結點已經宕掉了。另一方面,如果某個節點的時鐘被設定為過去的時間,也許另一節點就會認定那個節點已宕掉并試圖接過它的Job重運作。最簡單的同步計算機時鐘的方式是使用某一個Internet時間伺服器(Internet Time Server ITS)。
因為Quartz使用了一個随機的負載均衡算法, Job以随機的方式由不同的執行個體執行。Quartz官網上提到目前,還不存在一個方法來指派(釘住) 一個 Job 到叢集中特定的節點。
目前,如果不直接進到資料庫查詢的話,還沒有一個簡單的方式來得到叢集中所有正在執行的Job清單。請求一個Scheduler執行個體,将隻能得到在那個執行個體上正運作Job的清單。Quartz官網建議可以通過寫一些通路資料庫JDBC代碼來從相應的表中擷取全部的Job資訊。