?????? 在微服務環(huán)境下挠将,定時任務也需要獨立為一個服務。這里使用spring+quartz搭建定時任務開發(fā)環(huán)境。
?????? 在Config加載quartz.properties配置文件時砸烦,本地環(huán)境因為資源文件我們都存放在項目resource下冗尤,可使用ClassPathResource去拿到資源文件听盖。可是在集成裂七、測試皆看、生產(chǎn)環(huán)境下,一般會把配置文件都拿出來統(tǒng)一放在項目外的一個文件中背零,而ClassPathResource會從項目根目錄下開始查找資源腰吟,于是會拿不到項目外的quartz.properties,導致定時任務執(zhí)行可能會與預期結(jié)果不一致徙瓶,尤其是在集群環(huán)境中毛雇。讀取資源文件可采用PathResouce讀取配置文件的絕對路徑录语。
?????? 我們將調(diào)度信息存儲在mysql中,按照quartz規(guī)范在數(shù)據(jù)庫建立QRTZ_JOB_DETAILS,QRTZ_TRIGGERS等共11張表禾乘。建表sql可在quartz發(fā)型包中/docs/dbTables里看到澎埠。配置
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX?
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_?
quartz.properties有幾個配置可以注意下。集群的配置
org.quartz.jobStore.isClustered = true 開啟集群特性
org.quartz.jobStore.clusterCheckinInterval = 20000 設(shè)置Scheduler實例節(jié)點檢測頻率始藕,節(jié)點出現(xiàn)問題會被發(fā)現(xiàn)
org.quartz.jobStore.misfireThreshold = 60000 設(shè)置定時任務失火閾值蒲稳,當前時間超過原定執(zhí)行時間若是在閾值之內(nèi),就可以執(zhí)行
?????? 新建一個任務表用于存放我們配置要執(zhí)行的任務信息quartz_config表伍派,任務(組)名江耀,觸發(fā)器(組)名,執(zhí)行類诉植,cron表達式等祥国,可以在前端頁面對任務管理。在進行周期性任務狀態(tài)變化檢測時晾腔,需要取quartz_config內(nèi)的值來進行判斷舌稀。這里使用實現(xiàn)SchedulingConfigurer接口來完成動態(tài)定時任務
? ? /**
? ? * 執(zhí)行定時任務.
? ? */
? ? @Override
? ? public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
? ? ? ? taskRegistrar.addTriggerTask(
? ? ? ? ? ? ? ? //1.添加任務內(nèi)容(Runnable)
? ? ? ? ? ? ? ? () -> quartzManager.chickJobs(),
? ? ? ? ? ? ? ? //2.設(shè)置執(zhí)行周期(Trigger)
? ? ? ? ? ? ? ? triggerContext -> {
? ? ? ? ? ? ? ? ? ? //2.1 從數(shù)據(jù)庫獲取執(zhí)行周期
? ? ? ? ? ? ? ? ? ? String cron = env.getProperty("job.cron");
? ? ? ? ? ? ? ? ? ? //2.2 合法性校驗.
? ? ? ? ? ? ? ? ? ? if (StringUtils.isEmpty(cron)) {
? ? ? ? ? ? ? ? ? ? ? ? cron="0/5 * * * * ?";
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? //2.3 返回執(zhí)行周期(Date)
? ? ? ? ? ? ? ? ? ? return new CronTrigger(cron).nextExecutionTime(triggerContext);
? ? ? ? ? ? ? ? }
? ? ? ? );
? ? }
?????? 如圖,配置每分鐘檢測一次任務狀態(tài)變化灼擂,檢測功能在quartzManager.chickJobs()中實現(xiàn)壁查。目前我們設(shè)置Job的狀態(tài)有啟動,暫停剔应,刪除睡腿,刪除就邏輯刪除,頁面不展示峻贮,在quartz_config中以status字段來標示席怪。我們點擊啟動任務后,做的操作就將status置為1,此時雖然顯示已啟動纤控,可實際上是還未注冊實例的挂捻,然后等待下一次任務狀態(tài)檢測,取到status為1的任務數(shù)據(jù)嚼黔,然后使用scheduler.checkExists()檢測當前scheduler實例是否已經(jīng)存在該job,如果已經(jīng)存在细层,則獲取當前job實例的Cron表達式,判斷任務的觸發(fā)時間是否有變化唬涧,若有,則更新觸發(fā)器
? ? ? ? ? ? ? ? // 觸發(fā)器
? ? ? ? ? ? ? ? TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
? ? ? ? ? ? ? ? // 觸發(fā)器名,觸發(fā)器組
? ? ? ? ? ? ? ? triggerBuilder.withIdentity(triggerName, triggerGroupName);
? ? ? ? ? ? ? ? triggerBuilder.startNow();
? ? ? ? ? ? ? ? // 觸發(fā)器時間設(shè)定
? ? ? ? ? ? ? ? triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing());
? ? ? ? ? ? ? ? // 創(chuàng)建Trigger對象
? ? ? ? ? ? ? ? trigger = (CronTrigger) triggerBuilder.build();
? ? ? ? ? ? ? ? // 方式一 :修改一個任務的觸發(fā)時間
? ? ? ? ? ? ? ? sched.rescheduleJob(triggerKey, trigger);
否則添加一個job實例
?????? 動態(tài)檢測時對暫停和刪除的Job的處理邏輯是盛撑,先取出quartz_config中status不等于1的數(shù)據(jù)碎节,然后判斷scheduler中是否存在該Job,存在就證明該job還是已注冊的狀態(tài)抵卫,就將該job從調(diào)度器中移除狮荔。
? ? ? ? ? Scheduler sched = schedulerFactory.getScheduler();
? ? ? ? ? ? TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
? ? ? ? ? ? log.info("===============remove Job:{}===============",jobName);
? ? ? ? ? ? sched.pauseTrigger(triggerKey);// 停止觸發(fā)器
? ? ? ? ? ? sched.unscheduleJob(triggerKey);// 移除觸發(fā)器
? ? ? ? ? ? sched.deleteJob(JobKey.jobKey(jobName, jobGroupName));// 刪除任務
?????? 在集群多節(jié)點時胎撇,動態(tài)檢測管理job狀態(tài),還需要做進一步控制殖氏,之前就因為沒做控制晚树,在本地時,就一臺服務器雅采,一直運行無誤爵憎,找原因找了許久。假設(shè)集群中的兩臺服務器同時執(zhí)行了任務檢測邏輯婚瓜,此時有一個任務點擊啟動(status=1)宝鼓,正在等待檢測邏輯開始運行添加進實例(即insert進JOB_DETAILS,TRIGGERS等表),兩臺服務器同時拿到了這條待添加的job巴刻,必定有一臺服務器先將任務實例持久化到數(shù)據(jù)庫愚铡,另一臺服務器在執(zhí)行sched.scheduleJob(jobDetail,trigger)時,執(zhí)行到底層storeJob方法時胡陪,就會報出ObjectAlreadyExistsException異常
if (existingJob) {
? ? ? ? ? ? ? ? if (!replaceExisting) {
? ? ? ? ? ? ? ? ? ? throw new ObjectAlreadyExistsException(newJob);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? this.getDelegate().updateJobDetail(conn, newJob);
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? this.getDelegate().insertJobDetail(conn, newJob);
? ? ? ? ? ? }
?????? 我們的處理方法是沥寥,在quartz_config表中新增一個process_status字段,來標示當前任務處理狀態(tài)柠座,1為待處理营曼,2為處理中,3為處理完成愚隧,保證同時只有一個節(jié)點能執(zhí)行該添加修改操作蒂阱。quartz_config數(shù)據(jù)初始化改狀態(tài)為1,暫涂裉粒或修改Cron录煤,都會講process_status置為1,因為它是發(fā)生變化待檢測邏輯處理的荞胡。這樣的話妈踊,如上例,兩節(jié)點同時執(zhí)行下來泪漂,先到的一個會將process_status更新為2(處理中)廊营。
? ? ? UPDATE plms_quartz_config SET process_status = #{processStatus,jdbcType=VARCHAR} WHERE job_name = #{jobName,jdbcType=VARCHAR} AND process_status = '1'
更新成功則返回result = 1,只有result = 1的時候才會執(zhí)行接下來的添加修改操作萝勤。當前處理狀態(tài)已經(jīng)從1變成2了露筒,因為where條件的限制,另一節(jié)點到此已經(jīng)更新不到改狀態(tài)了敌卓,所以返回result = 0慎式,就不會再一次addJob或addTirgger,避免對象已存在異常。
?????? 有些情況下瘪吏,到了指定時間才觸發(fā)某個任務的執(zhí)行可能滿足不了需求癣防,我們需要能手動觸發(fā)一個任務立即執(zhí)行來完成有些特殊情況。立即觸發(fā)一個任務掌眠,我判斷了當前正在執(zhí)行中的任務不能立即執(zhí)行蕾盯。在任務執(zhí)行中,該任務真正觸發(fā)時間到了蓝丙,需要執(zhí)行级遭,會導致任務重復執(zhí)行,job類上因加上@DisallowConcurrentExecution防止任務重復執(zhí)行(集群都需要)迅腔。解決重復執(zhí)行了装畅,可是任務可能會因為misfire失火機制在空閑時間或者下個輪詢周期補償此次的錯失執(zhí)行〔琢遥看業(yè)務需要掠兄,配置失火策略,此處防止只執(zhí)行一次的任務多次執(zhí)行锌雀,我選擇了失火之后忽略該任務不做補償執(zhí)行,實現(xiàn)方法是在rescheduleJob或scheduleJob之前設(shè)置觸發(fā)器時蚂夕,如下:
riggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing());
此處手動觸發(fā)立即執(zhí)行任務
//執(zhí)行任務
? ? ? ? ? ? ? ? JobDetail job = JobBuilder.newJob((Class<? extends Job>) Class.forName(performJobReqDTO.getJobClassName())).withIdentity(jobKey).storeDurably().build();
? ? ? ? ? ? ? ? scheduler.addJob(job, true);
? ? ? ? ? ? ? ? scheduler.triggerJob(jobKey);
? ? ? ? ? ? ? ? log.info("job:{}任務已手動觸發(fā)",JSON.toJSONString(jobKey));
?????? 又遇到了個坑。此處立即執(zhí)行任務triggerJob觸發(fā)后腋逆,會在JOB_DETAILS婿牍,TRIGGERS,SIMPLE_TRIGGERS表存入數(shù)據(jù),觸發(fā)完后會刪除TRIGGER的數(shù)據(jù)惩歉,如若此時該任務正好處于剛點擊了啟動但是還未注冊的情況等脂,或者點立即執(zhí)行馬上又點擊啟動,因為立即執(zhí)行導致JobDetail已經(jīng)有該job的數(shù)據(jù)撑蚌,任務狀態(tài)檢測的時候就不會將該任務新注冊進去上遥,導致只有jobDetail,但缺失觸發(fā)器争涌,該任務就永遠不會執(zhí)行粉楚。處理方法為,若任務處理狀態(tài)為非處理完成亮垫,在立即執(zhí)行觸發(fā)后模软,清除該任務CronTrigger,SimpleTrigger,Trigger,JobDetail,當動態(tài)檢測執(zhí)行時,就能正常注冊任務觸發(fā)器饮潦。
?????? 又有個坑燃异,立即執(zhí)行觸發(fā)(scheduler.triggerJob(jobKey))后刪除那幾張表,可能會導致job實例不執(zhí)行害晦,任務觸發(fā)成功特铝,但是實際任務沒跑暑中,懷疑是triggerJob(jobKey)方法內(nèi)部執(zhí)行壹瘟,開啟一個線程后還需要去查表拿數(shù)據(jù)鲫剿,清楚太快導致沒拿到數(shù)據(jù),就沒跑成功稻轨,具體原因沒仔細研究灵莲,我在此處的解決方法是刪除之前讓線程睡一秒,確保任務能正常執(zhí)行殴俱。
?????? 本次也是初次對集群輪詢環(huán)境做這些大致的構(gòu)建政冻,做一下遇到的問題記錄筆記。當然以上處理方式還有很多更好更嚴謹?shù)奶幚矸绞接写齼?yōu)化线欲。