Author: Dorae
Date:2018年7月17日15:55:02
轉載請註明出處
一、quartz概述
quartz是一個用java實現的開源任務排程框架,可以用來建立簡單或者複雜的任務排程,並且可以提供許多企業級的功能,比如JTA以及叢集等,是當今比較流行的JAVA任務排程框架。
1. 可以用來做什麼
Quartz是一個任務排程框架,當遇到以下問題時:
- 想在每月25號,自動還款;
- 想在每年4月1日給當年自己暗戀的女神發一封匿名賀卡;
- 想每隔1小時,備份一下自己的各種資料。
那麼總結起來就是,在一個有規律的時間點做一些事情,並且這個規律可以非常複雜,複雜到了需要一個框架來幫助我們。Quartz的出現就是為了解決這個問題,定義一個觸發條件,那麼其負責到了特定的時間點,觸發相應的job幹活。
2. 特點
- 強大的排程功能,例如豐富多樣的排程方法,可以滿足各種常規和特殊需求;
- 靈活的應用方式,比如支援任務排程和任務的多種組合,支援資料的多種儲存(DB,RAM等;
- 支援分散式叢集,在被Terracotta收購之後,在原來基礎上進行了進一步的改造。
二、quartz基本原理
1. 核心元素
Quartz核心要素有Scheduler、Trigger、Job、JobDetail,其中trigger和job、jobDetail為後設資料,而Scheduler為實際進行排程的控制器。
- Trigger
Trigger用於定義排程任務的時間規則,在Quartz中主要有四種型別的Trigger:SimpleTrigger、CronTrigger、DataIntervalTrigger和NthIncludedTrigger。
- Job&Jodetail
Quartz將任務分為Job、JobDetail兩部分,其中Job用來定義任務的執行邏輯,而JobDetail用來描述Job的定義(例如Job介面的實現類以及其他相關的靜態資訊)。對Quartz而言,主要有兩種型別的Job,StateLessJob、StateFulJob
- Scheduler
實際執行排程邏輯的控制器,Quartz提供了DirectSchedulerFactory和StdSchedulerFactory等工廠類,用於支援Scheduler相關物件的產生。
2. 核心元素間關係
3. 主要執行緒
在Quartz中,有兩類執行緒,也即執行執行緒和排程執行緒,其中執行任務的執行緒通常用一個執行緒池維護。執行緒間關係如圖1-2所示。
圖 1-2
在quartz中,Scheduler排程執行緒主要有兩個:regular Scheduler Thread(執行常規排程)和Misfire Scheduler Thread(執行錯失的任務)。其中Regular Thread 輪詢Trigger,如果有將要觸發的Trigger,則從任務執行緒池中獲取一個空閒執行緒,然後執行與改Trigger關聯的job;Misfire Thraed則是掃描所有的trigger,檢視是否有錯失的,如果有的話,根據一定的策略進行處理。
4. 資料儲存
Quartz中的trigger和job需要儲存下來才能被使用。Quartz中有兩種儲存方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是將trigger和job儲存在記憶體中,而JobStoreSupport是基於jdbc將trigger和job儲存到資料庫中。RAMJobStore的存取速度非常快,但是由於其在系統被停止後所有的資料都會丟失,所以在叢集應用中,必須使用JobStoreSupport。其中表結構如表1-1所示。
Table name | Description |
---|---|
QRTZ_CALENDARS | 儲存Quartz的Calendar資訊 |
QRTZ_CRON_TRIGGERS | 儲存CronTrigger,包括Cron表示式和時區資訊 |
QRTZ_FIRED_TRIGGERS | 儲存與已觸發的Trigger相關的狀態資訊,以及相聯Job的執行資訊 |
QRTZ_PAUSED_TRIGGER_GRPS | 儲存已暫停的Trigger組的資訊 |
QRTZ_SCHEDULER_STATE | 儲存少量的有關Scheduler的狀態資訊,和別的Scheduler例項 |
QRTZ_LOCKS | 儲存程式的悲觀鎖的資訊 |
QRTZ_JOB_DETAILS | 儲存每一個已配置的Job的詳細資訊 |
QRTZ_SIMPLE_TRIGGERS | 儲存簡單的Trigger,包括重複次數、間隔、以及已觸的次數 |
QRTZ_BLOG_TRIGGERS | Trigger作為Blob型別儲存 |
QRTZ_TRIGGERS | 儲存已配置的Trigger的資訊 |
QRTZ_SIMPROP_TRIGGERS |
三、quartz叢集原理
一個Quartz叢集中的每個節點是一個獨立的Quartz應用,它又管理著其他的節點。這就意味著你必須對每個節點分別啟動或停止。Quartz叢集中,獨立的Quartz節點並不與另一其的節點或是管理節點通訊,而是通過相同的資料庫表來感知到另一Quartz應用的,如圖1-3所示。
四、quartz主要流程
1. 啟動流程
若quartz是配置在spring中,當伺服器啟動時,就會裝載相關的bean。SchedulerFactoryBean實現了InitializingBean介面,因此在初始化bean的時候,會執行afterPropertiesSet方法,該方法將會呼叫SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)建立Scheduler。SchedulerFactory在建立quartzScheduler的過程中,將會讀取配置引數,初始化各個元件,關鍵元件如下:
-
ThreadPool:一般是使用SimpleThreadPool,SimpleThreadPool建立了一定數量的WorkerThread例項來使得Job能夠線上程中進行處理。WorkerThread是定義在SimpleThreadPool類中的內部類,它實質上就是一個執行緒。在SimpleThreadPool中有三個list:workers-存放池中所有的執行緒引用,availWorkers-存放所有空閒的執行緒,busyWorkers-存放所有工作中的執行緒; 執行緒池的配置引數如下所示:
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount=3 org.quartz.threadPool.threadPriority=5
-
JobStore:分為儲存在記憶體的RAMJobStore和儲存在資料庫的JobStoreSupport(包括JobStoreTX和JobStoreCMT兩種實現,JobStoreCMT是依賴於容器來進行事務的管理,而JobStoreTX是自己管理事務),若要使用叢集要使用JobStoreSupport的方式;
-
QuartzSchedulerThread:用來進行任務排程的執行緒,在初始化的時候paused=true,halted=false,雖然執行緒開始執行了,但是paused=true,執行緒會一直等待,直到start方法將paused置為false;
另外,SchedulerFactoryBean還實現了SmartLifeCycle介面,因此初始化完成後,會執行start()方法,該方法將主要會執行以下的幾個動作:
- 建立ClusterManager執行緒並啟動執行緒:該執行緒用來進行叢集故障檢測和處理,將在下文詳細討論;
- 建立MisfireHandler執行緒並啟動執行緒:該執行緒用來進行misfire任務的處理,將在下文詳細討論;
- 置QuartzSchedulerThread的paused=false,排程執行緒才真正開始排程;
Quartz的整個啟動流程如圖1-4所示。
2. QuartzSchedulerThread執行緒
QuartzSchedulerThread執行緒是實際執行任務排程的執行緒,其中主要程式碼如下。
while (!halted.get()) {
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
triggers = qsRsrcs.getJobStore().acquireNextTriggers(now + idleWaitTime,
Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
long triggerTime = triggers.get(0).getNextFireTime().getTime();
long timeUntilTrigger = triggerTime - now;
while (timeUntilTrigger > 2) {
now = System.currentTimeMillis();
timeUntilTrigger = triggerTime - now;
}
List<TriggerFiredResult> bndle = qsRsrcs.getJobStore().triggersFired(triggers);
for (int i = 0; i < res.size(); i++) {
JobRunShell shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
shell.initialize(qs);
qsRsrcs.getThreadPool().runInThread(shell);
}
}
複製程式碼
- 先獲取執行緒池中的可用執行緒數量(若沒有可用的會阻塞,直到有可用的);
- 獲取30m內要執行的trigger(即acquireNextTriggers): 獲取trigger的鎖,通過select …for update方式實現;獲取30m內(可配置)要執行的triggers(需要保證叢集節點的時間一致),若@ConcurrentExectionDisallowed且列表存在該條trigger則跳過,否則更新trigger狀態為ACQUIRED(剛開始為WAITING);插入firedTrigger表,狀態為ACQUIRED;(注意:在RAMJobStore中,有個timeTriggers,排序方式是按觸發時間nextFireTime排的;JobStoreSupport從資料庫取出triggers時是按照nextFireTime排序);
- 等待直到獲取的trigger中最先執行的trigger在2ms內;
- triggersFired:
- 更新firedTrigger的status=EXECUTING;
- 更新trigger下一次觸發的時間;
- 更新trigger的狀態:無狀態的trigger->WAITING,有狀態的trigger->BLOCKED,若nextFireTime==null ->COMPLETE;
- commit connection,釋放鎖;
- 針對每個要執行的trigger,建立JobRunShell,並放入執行緒池執行:
- execute:執行job
- 獲取TRIGGER_ACCESS鎖
- 若是有狀態的job:更新trigger狀態:BLOCKED->WAITING,PAUSED_BLOCKED->BLOCKED
- 若@PersistJobDataAfterExecution,則updateJobData
- 刪除firedTrigger
- commit connection,釋放鎖
排程執行緒的執行流程如圖1-5所示。
排程過程中Trigger狀態變化如圖1-6所示。
3. MisfireHandler執行緒
下面這些原因可能造成 misfired job:
- 系統因為某些原因被重啟。在系統關閉到重新啟動之間的一段時間裡,可能有些任務會被 misfire;
- Trigger 被暫停(suspend)的一段時間裡,有些任務可能會被 misfire;
- 執行緒池中所有執行緒都被佔用,導致任務無法被觸發執行,造成 misfire;
- 有狀態任務在下次觸發時間到達時,上次執行還沒有結束;為了處理 misfired job,Quartz 中為 trigger 定義了處理策略,主要有下面兩種:
- MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:針對 misfired job 馬上執行一次;
- MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次觸發;預設是MISFIRE_INSTRUCTION_SMART_POLICY,該策略在CronTrigger中=MISFIRE_INSTRUCTION_FIRE_ONCE_NOW執行緒預設1分鐘執行一次;在一個事務中,預設一次最多recovery 20個;
執行流程:
- 若配置(預設為true,可配置)成獲取鎖前先檢查是否有需要recovery的trigger,先獲取misfireCount;
- 獲取TRIGGER_ACCESS鎖;
- hasMisfiredTriggersInState:獲取misfired的trigger,預設一個事務裡只能最大20個misfired trigger(可配置),misfired判斷依據:status=waiting,next_fire_time < current_time-misfirethreshold(可配置,預設1min)
- notifyTriggerListenersMisfired
- updateAfterMisfire:獲取misfire策略(預設是MISFIRE_INSTRUCTION_SMART_POLICY,該策略在CronTrigger中=MISFIRE_INSTRUCTION_FIRE_ONCE_NOW),根據策略更新nextFireTime;
- 將nextFireTime等更新到trigger表;
- commit connection,釋放鎖8.如果還有更多的misfired,sleep短暫時間(為了叢集負載均衡),否則sleep misfirethreshold時間,後繼續輪詢;
misfireHandler執行緒執行流程如圖1-7所示:
4. ClusterManager叢集管理執行緒
初始化:
failedInstance=failed+self+firedTrigger表中的schedulerName在scheduler_state表中找不到的(孤兒)
執行緒執行:
每個伺服器會定時(org.quartz.jobStore.clusterCheckinInterval這個時間)更新SCHEDULER_STATE表的LAST_CHECKIN_TIME,若這個欄位遠遠超出了該更新的時間,則認為該伺服器例項掛了;
注意:每個伺服器例項有唯一的id,若配置為AUTO,則為hostname+current_time
執行緒執行的具體流程:
- 檢查是否有超時的例項failedInstances;
- 更新該伺服器例項的LAST_CHECKIN_TIME; 若有超時的例項:
- 獲取STATE_ACCESS鎖;
- 獲取超時的例項failedInstances;
- 獲取TRIGGER_ACCESS鎖;
- clusterRecover:
- 針對每個failedInstances,通過instanceId獲取每個例項的firedTriggers;
- 針對每個firedTrigger:
- 更新trigger狀態:
- BLOCKED->WAITING
- PAUSED_BLOCKED->PAUSED
- ACQUIRED->WAITING
- 若firedTrigger不是ACQUIRED狀態(在執行狀態),且jobRequestRecovery=true: 建立一個SimpleTrigger,儲存到trigger表,status=waiting,MISFIRE_INSTR=MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY.
- 刪除firedTrigger
- 更新trigger狀態:
clusterManager執行緒執行時序圖如圖1-8所示:
五、注意問題
- 時間同步問題
Quartz實際並不關心你是在相同還是不同的機器上執行節點。當叢集放置在不同的機器上時,稱之為水平叢集。節點跑在同一臺機器上時,稱之為垂直叢集。對於垂直叢集,存在著單點故障的問題。這對高可用性的應用來說是無法接受的,因為一旦機器崩潰了,所有的節點也就被終止了。對於水平叢集,存在著時間同步問題。
節點用時間戳來通知其他例項它自己的最後檢入時間。假如節點的時鐘被設定為將來的時間,那麼執行中的Scheduler將再也意識不到那個結點已經宕掉了。另一方面,如果某個節點的時鐘被設定為過去的時間,也許另一節點就會認定那個節點已宕掉並試圖接過它的Job重執行。最簡單的同步計算機時鐘的方式是使用某一個Internet時間伺服器(Internet Time Server ITS)。
- 節點爭搶Job問題
因為Quartz使用了一個隨機的負載均衡演算法,Job以隨機的方式由不同的例項執行。Quartz官網上提到當前,還不存在一個方法來指派(釘住) 一個 Job 到叢集中特定的節點。
- 從叢集獲取Job列表問題
當前,如果不直接進到資料庫查詢的話,還沒有一個簡單的方式來得到叢集中所有正在執行的Job列表。請求一個Scheduler例項,將只能得到在那個例項上正執行Job的列表。Quartz官網建議可以通過寫一些訪問資料庫JDBC程式碼來從相應的表中獲取全部的Job資訊。