一、基本組件介紹
- Scheduler 任務調度器,按照特定的觸發規則,自動執行任務。
- Job 接口,定義需要執行的任務。
- JobDetail 包含job的基本信息。
- Trigger 描述Job執行的時間觸發規則。
- JobStore 存放Job、Trigger等信息。
Scheduler
Scheduler是一個任務調度器,保存JobDetail和Trigger的信息。 在Trigger觸發時,執行特定任務。
實現了org.quartz.Scheduler
接口的StdSchedule實際只是QuartzScheduler的代理,後者實現了Scheduler
所有操作。
創建
Scheduler由SchedulerFactory創建。
SchedulerFactory有兩個默認實現StdSchedulerFactory
和DirectSchedulerFactory
。
Scheduler的創建過程包括:
- 讀取配置文件, 配置文件中需要配置scheduler、線程池、jobStore、jobListener、triggerListenner、插件等。
- 配置文件的讀取過程如下:
- 讀取參數系統參數System中配置的org.quartz.properties指定的文件
- 如果找不到則讀取項目Classpath目錄下的quartz.properties配置文件
- 如果找不到則讀取quartz jar包中默認的配置文件quartz.properties
- 從SchedulerRepository中根據名稱讀取已經創建的scheduler,
- 如果沒有則重新創建一個,並保存在SchedulerRepository中。
存儲
Scheduler存儲在單例的SchedulerRepository中。
生命週期
Scheduler的生命週期開始於其被創建時,結束於shutdown()方法調用。一旦對象創建完成,就可以用來操作Jobs和Triggers,包括添加、刪除、查詢等。但只有在Scheduler start()被調用後,纔會按照Trigger定義的觸發規則執行Job的內容。
核心方法
Scheduler的核心功能就是操作Job、Trigger、Calendar、Listener等。包括addXXX、deleteXXX、pauseXXX、resumeXXX等。
Job
Job接口簡介
Job就是定時任務實實在在執行的內容,足夠單純,僅僅包含一個執行方法。
Job
JobExecutionContext對象包含了當前任務執行的上下文環境,包括JobDetail、Trigger以及jobDataMap等。
Job運行時環境
Job的執行並不是孤立封閉的,需用與外界交互。JobDataMap是一種擴展的Map<String,Object>結構,就是用來在任務調度器與任務執行之間傳遞數據。如果Job中包含了與JobDataMap中key值相對應的setter方法,那麼Scheduler容器將會在當前Job創建後自動調用該setter方法,完成數據傳遞,而不用hardcode的從map中取值。
Scheduler控制在每次Trigger觸發時創建Job實例。因此JobExecutionContext.JobDataMap只是外部Scheduler容器中JobDataMap的一個拷貝,即便修改Job中的JobDataMap也只是在當前Job執行的環境中生效,並不會對外部產生任何影響。
Job的派生
Job下面又派生出兩個子接口:InterruptableJob和StatefulJob
Job體系結構
InterruptableJob:可被阻斷的Job,InterruptableJob收到Scheduler.interrupt請求,停止任務
StatefulJob:有狀態Job,標識性接口,沒有操作方法。StatefulJob與普通的Job(無狀態Job)從根本上有兩點不同:
1. JobDataMap是共享的,即在Job中對JobDataMap的操作,將會被保存下來,其他Job拿到的將是被修改過的JobDataMap。
2. 基於第一條原因,StatefulJob是不允許併發執行的。
StatefulJob已被DisallowConcurrentExecution/PersistJobDataAfterExecution註解取代
Job的創建
Job的創建由專門的工廠來完成
Job Factory結構
Job是在Quartz內部創建,受Scheduler控制,因此不需要外部參與。
JobDetail
JobDetail用於保存Job相關的屬性信息
JobDetail結構
- JobKey唯一確定了一個Job
- JobDataMap用於存儲Job執行時必要的業務信息
- JobDetail保存的僅僅是Job接口的Class對象,而非具體的實例。
JobBuilder負責JobDetail實例的創建,並且JobBuilder提供了鏈式操作,可以方便的爲JobDetail添加額外的信息。
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity(jobKey)
.build();
Trigger
Trigger描述了Job的觸發規則。
Trigger
- TriggerKey(group,name)唯一標識了Scheduler中的Trigger
- JobKey指向了該Trigger作用的Job
- 一個Trigger僅能對應一個Job,而一個Job可以對應多個Trigger
- 同樣的,Trigger也擁有一個JobDataMap
- Priority:當多個trigger擁有相同的觸發時間,根據該屬性來確定先後順序,數值越大越靠前,默認5,可爲正負數
- Misfire Instructions:沒來得及執行的機制。同一時間trigger數量過多超過可獲得的線程資源,導致部分trigger無法執行。不同類型的Trigger擁有不同的機制。當Scheduler啓動時候,首先找到沒來得及執行的trigger,再根據不同類型trigger各自的處理策略處理
- Calendar:Quartz Calendar類型而不是java.util.Calendar類型。用於排除Trigger日程表中的特定時間範圍,比如原本每天執行的任務,排除非工作日
Trigger的幾種狀態
- STATE_WAITING(默認): 等待觸發
- STATE_ACQUIRED:
- STATE_COMPLETE:
- STATE_PAUSED:
- STATE_BLOCKED:
- STATE_PAUSED_BLOCKED:
- STATE_ERROR:
Trigger的分類
Trigger的分類
常見的兩種Trigger爲SimpleTrigger和CronTrigger.
SimpleTrigger
SimpleTrigger支持在特定時間點一次性執行或延遲執行N次,使用TriggerBuilder和SimpleScheduleBuilder創建
SimpleTrigger包含的屬性爲:
- startTime 開始時間
- endTime 如果指定的話,將會覆蓋repeat count
- repeat count 重複次數 >=0 int
- repeat interval 時間間隔(毫秒) >=0 long
CronTrigger
CronTrigger支持多次重複性複雜情況,支持Cron表達式,使用TriggerBuilder和CronScheduleBuilder創建。
Cron表達式由7部分組成,分別是秒 分 時 日期 月份 星期 年(可選),空格間隔。
字段 | 允許值 | 允許的特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小時 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1-12,JAN-DEC | , - * / |
星期 | 1-7,SUN-SAT | , - * ? / L C # |
年(可選) | 留空,1970-2099 | , - * / |
特殊字符含義
- "*": 代表所有可能的值
- "/": 用來指定數值的增量, 在子表達式(分鐘)裏的0/15表示從第0分鐘開始,每15分鐘;在子表達式(分鐘)裏的"3/20"表示從第3分鐘開始,每20分鐘(它和"3,23,43")的含義一樣
- "?": 僅被用於天和星期兩個子表達式,表示不指定值。當2個子表達式其中之一被指定了值以後,爲了避免衝突,需要將另一個子表達式的值設爲"?"
- "L": 僅被用於天和星期兩個子表達式,它是單詞"last"的縮寫。如果在“L”前有具體的內容,它就具有其他的含義了。例如:"6L"表示這個月的倒數第6天,"FRI L"表示這個月的最後一個星期五
- 'W' 可用於“日”字段。用來指定歷給定日期最近的工作日(週一到週五) 。比如你將“日”字段設爲"15W",意爲: "離該月15號最近的工作日"。因此如果15號爲週六,觸發器會在14號即週五調用。如果15號爲週日, 觸發器會在16號也就是週一觸發。如果15號爲週二,那麼當天就會觸發。然而如果你將“日”字段設爲"1W", 而一號又是週六, 觸發器會於下週一也就是當月的3號觸發,因爲它不會越過當月的值的範圍邊界。'W'字符只能用於“日”字段的值爲單獨的一天而不是一系列值的時候
- 'L'和'W'可以組合用於“日”字段表示爲'LW',意爲"該月最後一個工作日"。
- '#' 字符可用於“周幾”字段。該字符表示“該月第幾個周×”,比如"6#3"表示該月第三個週五( 6表示週五而"#3"該月第三個)。再比如: "2#1" = 表示該月第一個週一而 "4#5" = 該月第五個週三。注意如果你指定"#5"該月沒有第五個“周×”,該月是不會觸發的。
- 'C' 字符可用於“日”和“周幾”字段,它是"calendar"的縮寫。 它表示爲基於相關的日曆所計算出的值(如果有的話)。如果沒有關聯的日曆, 那它等同於包含全部日曆。“日”字段值爲"5C"表示"日曆中的第一天或者5號以後",“周幾”字段值爲"1C"則表示"日曆中的第一天或者週日以後"。
表達式舉例 - "0 0 12 * * ?" 每天中午12點觸發
- "0 15 10 ? * *" 每天上午10:15觸發
- "0 15 10 * * ?" 每天上午10:15觸發
- "0 15 10 * * ? *" 每天上午10:15觸發
- "0 15 10 * * ? 2005" 2005年的每天上午10:15觸發
- "0 * 14 * * ?" 在每天下午2點到下午2:59期間的每1分鐘觸發
- "0 0/5 14 * * ?" 在每天下午2點到下午2:55期間的每5分鐘觸發
- "0 0/5 14,18 * * ?" 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
- "0 0-5 14 * * ?" 在每天下午2點到下午2:05期間的每1分鐘觸發
- "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44觸發
- "0 15 10 ? * MON-FRI" 週一至週五的上午10:15觸發
- "0 15 10 15 * ?" 每月15日上午10:15觸發
- "0 15 10 L * ?" 每月最後一日的上午10:15觸發
- "0 15 10 ? * 6L" 每月的最後一個星期五上午10:15觸發
- "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最後一個星期五上午10:15觸發
- "0 15 10 ? * 6#3" 每月的第三個星期五上午10:15觸發
定時器正則表達式驗證
秒:^(\\*|[0-5]?[0-9]([,|\\-|\\/][0-5]?[0-9])*)$
分:^(\\*|[0-5]?[0-9]([,|\\-|\\/][0-5]?[0-9])*)$
時:^(\\*|([0-1]?[0-9]?|2[0-3])([,|\\-|\\/]([0-1]?[0-9]|2[0-3]))*)$
日期:^(\\*|\\?|(([1-9]|[1-2][0-9]|3[0-1])([,|\-|\/]([1-9]|[1-2][0-9]|3[0-1]))*)[CLW]?|[CLW]|LW)$
月份:^((\\*|[1-9]|(1[0-2]))([,|\-|\/]([1-9]|(1[0-2])))*)$
星期:^(\\*|L|\\?|[1-7](([,|\-|\/|\#][1-7])*|[LC]))$
年:^(\\*?|2[0-9]{3}([,|\-|\/]2[0-9]{3})*)$
Job Store
Job Store用於保存jobs, triggers等對應數據。JobStore的配置應在Quartz的配置文件中配置,代碼中應該避免直接操作JobStore實例
JobStroe的實現包括:
- RAMJobStore:把所有數據保存在內容中,速度快但不能持久化。配置org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
- JobStoreSupport:通過jdbc,以數據庫作爲存儲媒介
- JobStoreCMT: 使用應用服務器提供事務管理機制
- JobStoreTX: 不依賴與外部容器,可以自己提交、回滾事務
JobStore
RAMJobStore
RAMJobStore
JobDetail的存儲載體
JobWrapper
JobWrapper
Trigger的存儲載體
TriggerWrapper
// 存儲所有的TriggerWrapper
ArrayList<TriggerWrapper> triggers
// 以Trigger的group爲key,存儲TriggerKey<->TriggerWrapper形式的Map結構的Map
HashMap<String, HashMap<TriggerKey, TriggerWrapper>> triggersByGroup
// 以TriggerKey爲Key,存儲TriggerWrapper
HashMap<TriggerKey, TriggerWrapper> triggersByKey
// 即將被觸發的Trigger
TreeSet<TriggerWrapper> timeTriggers
TriggerWrapper
二、觸發器超時後的處理策略
2.1 背景
調度框架不能保證任務的百分百定時執行,所有經過某些原因,任務錯過默認的觸發時間時,需要有一定的策略去處理。
2.2 關鍵屬性misfireThreshold
在配置quartz.properties有這麼一個屬性就是misfireThreshold,用來指定調度引擎設置觸發器超時的"臨界值"。
要弄清楚觸發器超時臨界值的意義,那麼就要先弄清楚什麼是觸發器超時?打個比方:比如調度引擎中有5個線程,然後在某天的下午2點 有6個任務需要執行,那麼由於調度引擎中只有5個線程,所以在2點的時候會有5個任務會按照之前設定的時間正常執行,有1個任務因爲沒有線程資源而被延遲執行,這個就叫觸發器超時。
那麼超時的時間又是如何計算的呢?還接着上面的例子說,比如一個(任務A)應該在2點的時候執行,但是在2點的時候調度引擎中的可用線程都在忙碌狀態中,或者調度引擎掛了,這都有可能發生,然後再2點05秒的時候恢復(有可用線程或者服務器重新啓動),也就是說(任務A)應該在2點執行 但現在晚了5秒鐘。那麼這5秒鐘就是任務超時時間,或者叫做觸發器(Trigger)超時時間。
理解了上面的內容再來看misfireThreshold值的意義,misfireThreshold是用來設置調度引擎對觸發器超時的忍耐時間,簡單來說 假設misfireThreshold=6000(單位毫秒)。
那麼它的意思說當一個觸發器超時時間如果大於misfireThreshold的值 就認爲這個觸發器真正的超時(也叫Misfires)。
如果一個觸發器超時時間 小於misfireThreshold的值, 那麼調度引擎則不認爲觸發器超時。也就是說調度引擎可以忍受這個超時的時間。
還是 任務A 比它應該正常的執行時間晚了5秒 那麼misfireThreshold的值是6秒,那麼調度引擎認爲這個延遲時間可以忍受,所以不算超時(Misfires),那麼引擎會按照正常情況執行該任務。 但是如果 任務A 比它應該正常的執行時間晚了7秒 或者是6.5秒 只要大於misfireThreshold 那麼調度引擎就會認爲這個任務的觸發器超時。
2.3 觸發器超時後的處理策略
2.3.1我們在定義一個任務的觸發器時 最常用的就是倆種觸發器:1、SimpleTrigger 2、CronTrigger。
1、SimpleTrigger 默認的策略是 Trigger.MISFIRE_INSTRUCTION_SMART_POLICY 官方的解釋如下:
Instructs the Scheduler that upon a mis-fire situation, the updateAfterMisfire() method will be called on the Trigger to determine the mis-fire instruction, which logic will be trigger-implementation-dependent.
大意是:指示調度引擎在MisFire的狀態下,會去調用觸發器的updateAfterMisfire的方法來確定它的超時處理策略,裏面的邏輯取決於具體的實現類。
那我們在看一下updateAfterMisfire方法的說明:
If the misfire instruction is set to MISFIRE_INSTRUCTION_SMART_POLICY, then the following scheme will be used: If the Repeat Count is 0, then the instruction will be interpreted as MISFIRE_INSTRUCTION_FIRE_NOW. If the Repeat Count is REPEAT_INDEFINITELY, then the instruction will be interpreted as MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT. WARNING: using MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT with a trigger that has a non-null end-time may cause the trigger to never fire again if the end-time arrived during the misfire time span. If the Repeat Count is > 0, then the instruction will be interpreted as MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT.
大意是:
1、如果觸發器的重複執行數(Repeat Count)等於0,那麼會按這個(MISFIRE_INSTRUCTION_FIRE_NOW)策略執行。
2、如果觸發器的重複執行次數是 SimpleTrigger.REPEAT_INDEFINITELY (常量值爲-1,意思是重複無限次) ,那麼會按照MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT策略執行。
3、如果觸發器的重複執行次數大於0,那麼按照 MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT執行。
既然是這樣,那就讓我們依次看一下每種處理策略都是啥意思!
1、MISFIRE_INSTRUCTION_FIRE_NOW
Instructs the
that upon a mis-fire situation, the
Scheduler
wants to be fired now by
SimpleTrigger
Scheduler
.NOTE: This instruction should typically only be used for 'one-shot' (non-repeating) Triggers. If it is used on a trigger with a repeat count > 0 then it is equivalent to the instruction
.
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
指示調度引擎在MisFire的情況下,將任務(JOB)馬上執行一次。
需要注意的是 這個指令通常被用做只執行一次的Triggers,也就是沒有重複的情況(non-repeating),如果這個Triggers的被安排的執行次數大於0
那麼這個執行與 (4)MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT 相同
2、
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
|
|
指示調度引擎重新調度該任務,repeat count保持不變,並且服從trigger定義時的endTime,如果現在的時間,如果當前時間已經晚於 end-time,那麼這個觸發器將不會在被激發。
注意:這個狀態會導致觸發器忘記最初設置的 start-time 和 repeat-count,爲什麼這個說呢,看源代碼片段:updateAfterMisfire方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
getTimesTriggered的是獲取這個觸發器已經被觸發了多少次,那麼用原來的次數 減掉 已經觸發的次數就是還要觸發多少次
接下來就是判斷一下觸發器是否到了結束時間,如果到了的話,觸發器就不會在被觸發。
然後就是重新設置觸發器的開始實現是 “現在” 並且立即運行。
3、MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
1 2 3 |
|
這個策略跟上面的2策略一樣,唯一的區別就是設置觸發器的時間 不是“現在” 而是下一個 scheduled time。解釋起來比較費勁,舉個例子就能說清楚了。
比如一個觸發器設置的時間是 10:00 執行 時間間隔10秒 重複10次。那麼當10:07秒的時候調度引擎可以執行這個觸發器的任務。那麼如果是策略(2),那麼任務會立即運行。
那麼觸發器的觸發時間就變成了 10:07 10:17 10:27 10:37 .....
那麼如果是策略(3),那麼觸發器會被安排在下一個scheduled time。 也就是10:20觸發。 然後10:30 10:40 10:50。這回知道啥意思了吧。
4、MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
這個策略跟上面的策略(2)比較類似,指示調度引擎重新調度該任務,repeat count 是剩餘應該執行的次數,也就是說本來這個任務應該執行10次,但是已經錯過了3次,那麼這個任務就還會執行7次。
下面是這個策略的源碼,主要看紅色的地方就能看到與策略(2)的區別,這個任務的repeat count 已經減掉了錯過的次數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
5、MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
1 2 3 |
|
這個策略與上面的策略3比較類似,區別就是repeat count 是剩餘應該執行的次數而不是全部的執行次數。比如一個任務應該在2:00執行,repeat count=5,時間間隔5秒, 但是在2:07才獲得執行的機會,那任務不會立即執行,而是按照機會在2點10秒執行。
6、MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
|
|
這個策略是忽略所有的超時狀態,和最上面講到的 (第二種情況) 一致。
舉個例子,一個SimpleTrigger 每個15秒鐘觸發, 但是超時了5分鐘才獲得執行的機會,那麼這個觸發器會被快速連續調用20次, 追上前面落下的執行次數。
2.3.2、CronTrigger 的默認策略也是Trigger.MISFIRE_INSTRUCTION_SMART_POLICY
官方解釋如下,也就是說不指定的話默認爲:MISFIRE_INSTRUCTION_FIRE_ONCE_NOW。
1 2 3 4 |
|
1、MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
1 |
|
這個策略指示觸發器超時後會被立即安排執行,看源碼,紅色標記的地方。也就是說不管這個觸發器是否超過結束時間(endTime) 首選執行一次,然後就按照正常的計劃執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
2、MISFIRE_INSTRUCTION_DO_NOTHING
這個策略與策略(1)正好相反,它不會被立即觸發,而是獲取下一個被觸發的時間,並且如果下一個被觸發的時間超出了end-time 那麼觸發器就不會被執行。
上面綠色標記的地方是源碼
補充幾個方法的說明:
1、getFireTimeAfter 返回觸發器下一次將要觸發的時間,如果在給定(參數)的時間之後,觸發器不會在被觸發,那麼返回null。
1 2 |
|
2、isTimeIncluded 判斷給定的時間是否包含在quartz的日曆當中,因爲quartz是可以自定義日曆的,設置哪些日子是節假日什麼的。
1 2 |
|
三、踩坑記錄
Scheduler不要使用@Autowired自動注入,要使用創建StdSchedulerFactory類對象進行獲取,因爲自動注入的Scheduler的misfireThreshold默認是5000ms,而通過StdSchedulerFactory類對象獲取的Scheduler,misfireThreshold的值是60000ms。