概述
Quartz
一款功能豐富、歷史悠久,完全基于Java實現(xiàn)的開源任務調(diào)度框架,Java調(diào)度領域知名度非常高。其簡單易用、穩(wěn)定可靠的特性,使其被很多第三方應用將其當成調(diào)度框架基礎依賴,如spring boot
已內(nèi)置集成quartz
,elastic-job
調(diào)度框架則將quartz
作為其底層基礎實現(xiàn)進行封裝,xxl-job
曾經(jīng)歷史版本也是集成quartz
作為其觸發(fā)實現(xiàn)機制基礎,不過在最新版本采用時間輪實現(xiàn)已將quartz
移除。
核心三叉戟
使用quartz api
時,最核心三件套如下:
Scheduler
SchedulerFactory
和Scheduler
從名稱就很容易識別這里采用工廠設計模式,Scheduler
是quartz
暴露出來供開發(fā)使用的一個最重要組件,從開發(fā)者視角來看它就是quartz
的門面,對quartz
的各種操作都是通過Scheduler
進行串聯(lián),類似于quartz
的大管家、代言人角色。
【資料圖】
“這種設計模式在開源框架中很常見,比如
mybatis
中SqlSessionFactory
和SqlSession
,通過給開發(fā)者提供大管家組件,通過一個組件串聯(lián)起所有核心功能,簡化了開發(fā)人員上手框架難度。
一般一個應用只會對應一個Scheduler
實例,不同Scheduler
實例之間通過schedulerName
進行隔離,所有的quartz
數(shù)據(jù)庫表設計中都有sched_name
這一列字段,這樣Scheduler處理任務時只會操作數(shù)據(jù)庫表中對應schedulerName
下的數(shù)據(jù)。quartz
集群就是利用多個Scheduler
實例配置相同schedulerName
名稱,實現(xiàn)多機器同時處理同一個schedulerName
下任務來達到集群效果。
“
schedulerName
可以通過org.quartz.scheduler.instanceName
進行配置,默認名稱為QuartzScheduler
。
Scheduler
操作的主要是JobDetail
和Trigger
兩個組件,JobDetail
封裝的是任務配置信息,而Trigger
觸發(fā)器封裝了任務觸發(fā)信息,它們是1:N
關系,即一個JobDetail
可以關聯(lián)多個Trigger
觸發(fā)器,但是一個Trigger
觸發(fā)器只能綁定到一個Job上。
JobDetail
JobDetail
組件封裝了quartz
調(diào)度任務定義信息,下面是JobDetail
組件常規(guī)使用方式如下:
// JobDataMap實現(xiàn)Map接口,任務調(diào)度時存儲到JobExecuteContext中,可以傳遞給Job實例JobDataMap jobDataMap = new JobDataMap();jobDataMap.put("name", "zhangsan");jobDataMap.put("time", System.currentTimeMillis());JobDetail jobDetail = JobBuilder // 綁定任務類 .newJob(QuartzCronJob.class) .storeDurably() // job對應ID .withIdentity("job2", "DEFAULT") .usingJobData(jobDataMap) .build();JobKey jobKey = jobDetail.getKey();if (scheduler.checkExists(jobKey)) { log.warn("調(diào)度任務已存在,刪除后重新添加:{}", jobKey); scheduler.interrupt(jobKey);//停止JOB /** * deleteJob操作在刪除Job之前,會執(zhí)行unscheduleJob()取消job和trigger關聯(lián) */ scheduler.deleteJob(jobKey);}// 將JobDetail任務定義信息插入quartz表scheduler.addJob(jobDetail, true);
JobDetail
操作比較簡單,主要有兩點需要注意:1、newJob(Class extends Job> jobClass)
操作綁定任務類,任務類就是封裝用戶業(yè)務邏輯類;2、withIdentity(String name, String group)
給該任務設置一個身份ID,后續(xù)可以通過該身份ID進行管理,為方便靈活管理quartz
抽象出group
概念,這樣可以批量對一組作業(yè)進行批量操作,身份ID使用JobKey
進行封裝。
使用Scheduler
類addJob(JobDetail jobDetail, boolean replace)
方法就將創(chuàng)建的Job
定義信息添加到quartz
中,一般采用數(shù)據(jù)庫持久化模式,即這里就會將Job
定義信息插入到qrtz_job_details
表中(見下圖)。
下面來看下幾個關鍵字段:
sched_name:上面說過,用來關聯(lián)對應的Scheduler實例is_durable:是否持久化is_nonconcurrent:是否允許同一個作業(yè)可以同時多個實例執(zhí)行,比如一個任務間隔1秒,但其執(zhí)行時間為2秒,通過該屬性控制是否允許同一個作業(yè)有多個任務同時允許,參見@DisallowConcurrentExecutionis_update_data: 任務已經(jīng)執(zhí)行中,是否允許更新JobDataMap持久化信息,參見@PersistJobDataAfterExecutionrequests_recovery: 故障恢復使用,具體參見后續(xù)源碼分析job_data:JobDataMap序列化后存儲到字段中
Trigger
任務定義完成,但是任務按照怎么周期性規(guī)則進行觸發(fā)執(zhí)行,這就要看Trigger
觸發(fā)器的臉色了
Trigger
組件常規(guī)使用方式如下:
JobDataMap jobDataMap = new JobDataMap();jobDataMap.put("name", "lisi");jobDataMap.put("address", "China");Trigger trigger = TriggerBuilder .newTrigger() .withIdentity("trigger1", "DEFAULT") .usingJobData(jobDataMap) .startAt(new Date()) .endAt(new Date(System.currentTimeMillis()+38 * 60 * 1000)) .withSchedule(CronScheduleBuilder.cronSchedule("*/10 * * * * ?")) .forJob(new JobKey("job1", "DEFAULT")) .build();//時間TriggerKey triggerKey = trigger.getKey();if(scheduler.checkExists(triggerKey)){ scheduler.unscheduleJob(triggerKey);}//必須綁定jobscheduler.scheduleJob(trigger);
和JobDetail
類似,主要有兩點需要注意:1、同withIdentity(String name, String group)
,同理給該觸發(fā)器設置一個身份ID,對應TriggerKey
;2、startAt()
、endAt()
對應啟止時間;3、withSchedule(CronScheduleBuilder.cronSchedule("*/10 * * * * ?"))
;4、forJob(JobKey keyOfJobToFire)
將Trigger
與Job
進行關聯(lián),這樣才知道觸發(fā)哪個任務。
最后通過Scheduler
類scheduleJob(Trigger trigger)
方法就將創(chuàng)建的Trigger
定義信息添加到quartz
中,一般采用數(shù)據(jù)庫持久化模式,即這里就會將Trigger
定義信息插入到觸發(fā)器相關表中,示例中使用cron
觸發(fā)器,則插入到qrtz_cron_triggers
表中(見下圖)。
下面我們就來看下任務是咋個觸發(fā)的。Scheduler
類scheduleJob(Trigger trigger)
將觸發(fā)器持久化后,你會發(fā)現(xiàn)qrtz_cron_triggers
中沒有起止時間以及和Job
綁定內(nèi)容,所以,接下來我們看一張非常重要表:qrtz_triggers
。scheduleJob()
方法在持久化Trigger
信息后會同時向qrtz_triggers
表插入一條記錄(見下圖):
qrtz_job_details
和qrtz_cron_triggers
可以看成靜態(tài)表,那qrtz_triggers
就是運行動態(tài)表,保存著任務運行期間數(shù)據(jù),且隨著運行記錄在動態(tài)變更,是quartz
調(diào)度任務運行最重要的一張表,下面我們來看下這張表中幾個關鍵字段:
start_time、end_time: trigger定義時設置的起止時間next_fire_time: 下次觸發(fā)時間戳prev_fire_time: 上次觸發(fā)時間戳trigger_state: trigger狀態(tài),最常見狀態(tài)WAITING、ACQUIRED和EXECUTING,分別對應等待(下次觸發(fā)時間還早) -> 加載到內(nèi)存中等待(下次觸發(fā)時間快到了) --> 執(zhí)行(下次觸發(fā)時間到了,需要觸發(fā)任務),具體參見后續(xù)源碼分析misfire_instr: trigger觸發(fā)時間過期處理策略,比如本來是10:23:50時間點進行觸發(fā),但是由于某些原因在10:23:53秒才檢索出來,這是該觸發(fā)時間點已經(jīng)過期,misfire_instr就是控制采用什么策略處理該過期任務,是直接丟棄重新計算下次觸發(fā)時間點、還是一定時間范圍內(nèi)過期的理解執(zhí)行等等,具體參見后續(xù)源碼分析job_data: 和JobDetail一樣,Trigger也可綁定一個JobDataMap,用于向Job實例傳遞參數(shù),該字段就是存儲Trigger關聯(lián)的JobDataMap序列化內(nèi)容
quartz
基本上就是圍繞qrtz_triggers
中這幾個關鍵字段實現(xiàn)任務觸發(fā),我們連蒙帶猜大致可以想出quartz
任務調(diào)度觸發(fā)機制粗略流程:
1、通過配置的trigger
觸發(fā)器,計算出下次觸發(fā)時間,更新到next_fire_time
字段,同時更新trigger_state
狀態(tài)為WAITING
;
2、quartz
線程掃描該表,從表中查詢出未來很短一段時間將要觸發(fā)的記錄(比對next_fire_time
和當前時間)放入到內(nèi)存排隊隊列中,然后將trigger_state
更新成ACQUIRED
;
3、然后阻塞直到內(nèi)存排隊隊列中觸發(fā)任務到時間點,再觸發(fā)任務之前,重新計算下次觸發(fā)時間點,更新到next_fire_time
,同時將trigger_state
更新為WAITING
,然后執(zhí)行當前任務;
4、由于next_fire_time
和trigger_state
值更新,重新開始步驟1,就這樣循環(huán)往復觸發(fā)下去。
總結(jié)
這節(jié)從一個使用者角度簡單分析quartz
核心運行機制,由于只是簡單的從外層而未深入剖析源碼,只是簡單結(jié)合數(shù)據(jù)庫表信息對quartz
大致的運行機制做個簡單猜想,一些重要屬性也沒展開,帶著這些疑問下一節(jié)通過源碼分析找到真實的答案,一步步加深對quartz
運行機制的理解。
關鍵詞: