Quartz原理解密

Quartz原理解密

Author: Dorae
Date:2018年7月17日15:55:02
轉載請注明出處

由于切換博客,2018/09/20及之前文章可能會存在圖片無法查看的問題,請移步這里。


一、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. 核心元素間關系

[圖片上傳失敗...(image-7b2a3e-1537434818927)]

<center><img src="http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_%E5%9B%BE%201-1.png"/></center>

<center>圖 1-1</center>

3. 主要線程

在Quartz中,有兩類線程,也即執行線程和調度線程,其中執行任務的線程通常用一個線程池維護。線程間關系如圖1-2所示。

<center><img src="http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_1-2.png"/></center>

<center>圖 1-2</center>

圖 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所示。

<center>表 1-1</center>

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所示。

<center><img src="http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_1-3.png"/></center>

<center>圖 1-3</center>

四、quartz主要流程

1. 啟動流程

若quartz是配置在spring中,當服務器啟動時,就會裝載相關的bean。SchedulerFactoryBean實現了InitializingBean接口,因此在初始化bean的時候,會執行afterPropertiesSet方法,該方法將會調用SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)創建Scheduler。SchedulerFactory在創建quartzScheduler的過程中,將會讀取配置參數,初始化各個組件,關鍵組件如下:

  1. 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
  1. JobStore:分為存儲在內存的RAMJobStore和存儲在數據庫的JobStoreSupport(包括JobStoreTX和JobStoreCMT兩種實現,JobStoreCMT是依賴于容器來進行事務的管理,而JobStoreTX是自己管理事務),若要使用集群要使用JobStoreSupport的方式;

  2. QuartzSchedulerThread:用來進行任務調度的線程,在初始化的時候paused=true,halted=false,雖然線程開始運行了,但是paused=true,線程會一直等待,直到start方法將paused置為false;

另外,SchedulerFactoryBean還實現了SmartLifeCycle接口,因此初始化完成后,會執行start()方法,該方法將主要會執行以下的幾個動作:

  1. 創建ClusterManager線程并啟動線程:該線程用來進行集群故障檢測和處理,將在下文詳細討論;
  2. 創建MisfireHandler線程并啟動線程:該線程用來進行misfire任務的處理,將在下文詳細討論;
  3. 置QuartzSchedulerThread的paused=false,調度線程才真正開始調度;

Quartz的整個啟動流程如圖1-4所示。

<center><img src = "http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_1-4.png"/></center>

<center>圖 1-4</center>

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);
    }
}
  1. 先獲取線程池中的可用線程數量(若沒有可用的會阻塞,直到有可用的);
  2. 獲取30m內要執行的trigger(即acquireNextTriggers):
    獲取trigger的鎖,通過select …for update方式實現;獲取30m內(可配置)要執行的triggers(需要保證集群節點的時間一致),若@ConcurrentExectionDisallowed且列表存在該條trigger則跳過,否則更新trigger狀態為ACQUIRED(剛開始為WAITING);插入firedTrigger表,狀態為ACQUIRED;(注意:在RAMJobStore中,有個timeTriggers,排序方式是按觸發時間nextFireTime排的;JobStoreSupport從數據庫取出triggers時是按照nextFireTime排序);
  3. 等待直到獲取的trigger中最先執行的trigger在2ms內;
  4. triggersFired:
    1. 更新firedTrigger的status=EXECUTING;
    2. 更新trigger下一次觸發的時間;
    3. 更新trigger的狀態:無狀態的trigger->WAITING,有狀態的trigger->BLOCKED,若nextFireTime==null ->COMPLETE;
    4. commit connection,釋放鎖;
  5. 針對每個要執行的trigger,創建JobRunShell,并放入線程池執行:
    1. execute:執行job
    2. 獲取TRIGGER_ACCESS鎖
    3. 若是有狀態的job:更新trigger狀態:BLOCKED->WAITING,PAUSED_BLOCKED->BLOCKED
    4. 若@PersistJobDataAfterExecution,則updateJobData
    5. 刪除firedTrigger
    6. commit connection,釋放鎖

調度線程的執行流程如圖1-5所示。

<center><img src="http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_1-5.png"/></center>

<center>圖 1-5</center>

調度過程中Trigger狀態變化如圖1-6所示。

<center><img src="http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_1-6.jpg"/></center>

<center>圖 1-6</center>

3. MisfireHandler線程

下面這些原因可能造成 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,等待下次觸發;默認是MISFIRE_INSTRUCTION_SMART_POLICY,該策略在CronTrigger中=MISFIRE_INSTRUCTION_FIRE_ONCE_NOW線程默認1分鐘執行一次;在一個事務中,默認一次最多recovery 20個;

執行流程:

  1. 若配置(默認為true,可配置)成獲取鎖前先檢查是否有需要recovery的trigger,先獲取misfireCount;
  2. 獲取TRIGGER_ACCESS鎖;
  3. hasMisfiredTriggersInState:獲取misfired的trigger,默認一個事務里只能最大20個misfired trigger(可配置),misfired判斷依據:status=waiting,next_fire_time < current_time-misfirethreshold(可配置,默認1min)
  4. notifyTriggerListenersMisfired
  5. updateAfterMisfire:獲取misfire策略(默認是MISFIRE_INSTRUCTION_SMART_POLICY,該策略在CronTrigger中=MISFIRE_INSTRUCTION_FIRE_ONCE_NOW),根據策略更新nextFireTime;
  6. 將nextFireTime等更新到trigger表;
  7. commit connection,釋放鎖8.如果還有更多的misfired,sleep短暫時間(為了集群負載均衡),否則sleep misfirethreshold時間,后繼續輪詢;

misfireHandler線程執行流程如圖1-7所示:

<center><img src= "http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_1-7.png"/></center>

<center>圖 1-7</center>

4. ClusterManager集群管理線程

初始化:

failedInstance=failed+self+firedTrigger表中的schedulerName在scheduler_state表中找不到的(孤兒)

線程執行:

每個服務器會定時(org.quartz.jobStore.clusterCheckinInterval這個時間)更新SCHEDULER_STATE表的LAST_CHECKIN_TIME,若這個字段遠遠超出了該更新的時間,則認為該服務器實例掛了;

注意:每個服務器實例有唯一的id,若配置為AUTO,則為hostname+current_time

線程執行的具體流程:

  1. 檢查是否有超時的實例failedInstances;
  2. 更新該服務器實例的LAST_CHECKIN_TIME;
    若有超時的實例:
  3. 獲取STATE_ACCESS鎖;
  4. 獲取超時的實例failedInstances;
  5. 獲取TRIGGER_ACCESS鎖;
  6. 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

clusterManager線程執行時序圖如圖1-8所示:

<center><img src="http://images.cnblogs.com/cnblogs_com/Dorae/1258051/o_1-8.png"/></center>

<center>圖 1-8</center>

五、注意問題

  1. 時間同步問題

Quartz實際并不關心你是在相同還是不同的機器上運行節點。當集群放置在不同的機器上時,稱之為水平集群。節點跑在同一臺機器上時,稱之為垂直集群。對于垂直集群,存在著單點故障的問題。這對高可用性的應用來說是無法接受的,因為一旦機器崩潰了,所有的節點也就被終止了。對于水平集群,存在著時間同步問題。

節點用時間戳來通知其他實例它自己的最后檢入時間。假如節點的時鐘被設置為將來的時間,那么運行中的Scheduler將再也意識不到那個結點已經宕掉了。另一方面,如果某個節點的時鐘被設置為過去的時間,也許另一節點就會認定那個節點已宕掉并試圖接過它的Job重運行。最簡單的同步計算機時鐘的方式是使用某一個Internet時間服務器(Internet Time Server ITS)。

  1. 節點爭搶Job問題

因為Quartz使用了一個隨機的負載均衡算法,Job以隨機的方式由不同的實例執行。Quartz官網上提到當前,還不存在一個方法來指派(釘住) 一個 Job 到集群中特定的節點。

  1. 從集群獲取Job列表問題

當前,如果不直接進到數據庫查詢的話,還沒有一個簡單的方式來得到集群中所有正在執行的Job列表。請求一個Scheduler實例,將只能得到在那個實例上正運行Job的列表。Quartz官網建議可以通過寫一些訪問數據庫JDBC代碼來從相應的表中獲取全部的Job信息。

六、參考文獻

  1. https://www.cnblogs.com/drift-ice/p/3817269.html
  2. https://www.cnblogs.com/zhenyuyaodidiao/p/4755649.html
  3. https://blog.csdn.net/u014427391/article/details/79087865#%E7%AC%AC%E5%9B%9B%E7%AB%A0-job%E4%BB%BB%E5%8A%A1
  4. https://blog.csdn.net/moon_yang_bj/article/details/55510494
  5. https://my.oschina.net/songhongxu/blog/802574
  6. http://wangtianzhi.cn/2016/01/03/quartz-source-analysis/
  7. http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容

  • 概述 了解Quartz體系結構 Quartz對任務調度的領域問題進行了高度的抽象,提出了調度器、任務和觸發器這3個...
    張晨輝Allen閱讀 2,241評論 2 11
  • ** 版本:2.2.1 ** Hello world: 調度器: 任務詳情:任務體實現Job接口 觸發器: 執行調...
    Coselding閱讀 10,205評論 12 38
  • 個人專題目錄[http://www.lxweimin.com/u/2a55010e3a04] 一、實現方法 spr...
    Java及SpringBoot閱讀 1,437評論 0 0
  • 帶著殷切的思念回家 思念便在到達目的時融化 融化成熟悉的飯菜和 安詳的睡眠 睡眠里沒有夢 我的思念依然在 夢帶著我...
    楊云濤閱讀 201評論 0 0
  • 和舒服的人待在一起是多么幸福的事 即便兩人一路不說話,也很舒服、自在,像云卷云舒、花開花落,往事回想起來,像恰到好...
    鐘無迭笙閱讀 186評論 1 2