使用Quartz實現定時任務
一:Quertz的用途
Quertz是一個開源的作業任務排程框架,他可以完成像JavaScript定時器類式的功能,其實Java中Timer也可實現部分功能,但相比Quertz還是略遜一籌,本人這次需要解決的就是定期統計消費記錄的功能。你還可以用他完成定期執行各類操作的功能。比如
-
- 想每月25號,信用卡自動還款
- 想每年4月1日自己給當年暗戀女神發一封匿名賀卡
- 想每隔1小時,備份一下自己的學習筆記到雲盤
這些問題總結起來就是:
在某一個有規律的時間點幹某件事。並且時間的觸發的條件可以非常複雜(比如每月最後一個工作日的17:50),複雜到需要一個專門的框架來幹這個事。
Quartz就是來幹這樣的事,你給它一個觸發條件的定義,它負責到了時間點,觸發相應的Job起來幹活。
二:使用方法(主要是Spring整合使用)
採用Spring整合Quartz使用程式碼方式或者xml方式都可以,我這裡也提供兩種方式,名稱相同適合對比學習。
程式碼方式
1:引入配置,pom.xml檔案引入下列兩個路徑(非Maven可自行配置)
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz --> <dependency> <Pom.xmlgroupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support --> <dependency><groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.1.0.RELEASE</version> </dependency>
2:建立【com.xqc.campusshop.config.quartz】包進行相關配置
(我知道大家不喜歡看原始碼,但是我還是得說看原始碼效果好)原始碼中
productSellDailyService為定期統計消費記錄Service介面
dailyCalculate 為ProductSellDailyService介面中執行定期統計的的方法,
triggerFactory.setCronExpression("? 0 0 * * ? *");為定時的時間,可訪問線上cron表示式生成器生成相應時間 http://cron.qqe2.com/
1 package com.xqc.campusshop.config.quartz; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.context.annotation.Configuration; 6 import org.springframework.scheduling.quartz.CronTriggerFactoryBean; 7 import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean; 8 import org.springframework.scheduling.quartz.SchedulerFactoryBean; 9 10 import com.xqc.campusshop.service.ProductSellDailyService; 11 12 @Configuration 13 public class QuartzConfiguration { 14 15 //定期統計消費記錄Service介面 16 @Autowired 17 private ProductSellDailyService productSellDailyService; 18 19 @Autowired 20 private MethodInvokingJobDetailFactoryBean jobDetailFactory; 21 22 @Autowired 23 private CronTriggerFactoryBean productSellDailyTriggerFactory; 24 25 /** 26 * 建立jobDetail並返回 27 * @return 28 */ 29 @Bean(name="jobDetailFactory") 30 public MethodInvokingJobDetailFactoryBean crateJobDetail(){ 31 32 //new出jobDetailFactory物件,此工廠主要用來製作一個jobDetail,及製作一個任務 33 //由於我們所做的定時任務根本上講其實就是執行一個方法,所以這個工廠比較方便 34 MethodInvokingJobDetailFactoryBean jobDetailFactoryBean = new MethodInvokingJobDetailFactoryBean(); 35 //設定jobDetail的名字 36 jobDetailFactoryBean.setName("product_sell_daily_job"); 37 //設定jobDetail的組名 38 jobDetailFactoryBean.setGroup("job_product_sell_daily_group"); 39 //對於相同的JobDetail,當指定多個Triggger時,很可能第一個job完成以前,第二個job就開始了 40 //指定設為false,多個job則不會併發執行,第二個job不會再第一個job完成前開始 41 jobDetailFactoryBean.setConcurrent(false); 42 //指定執行任務的類 43 jobDetailFactoryBean.setTargetObject(productSellDailyService); 44 //指定執行任務的方法 45 jobDetailFactoryBean.setTargetMethod("dailyCalculate"); 46 47 return jobDetailFactoryBean; 48 } 49 50 /** 51 * 建立cronTriggerFactory並返回 52 * 53 * @return 54 */ 55 @Bean("productSellDailyTriggerFactory") 56 public CronTriggerFactoryBean createProductSellDailyTrigger(){ 57 //建立TriggerFactory例項,用來建立trigger 58 CronTriggerFactoryBean triggerFactory = new CronTriggerFactoryBean(); 59 //設定triggerFactory的名字 60 triggerFactory.setName("product_sell_daily_trigger"); 61 //設定組名 62 triggerFactory.setGroup("job_product_sell_daily_group"); 63 //繫結jobDetail 64 triggerFactory.setJobDetail(jobDetailFactory.getObject()); 65 //設定cron表示式,請訪問:http://cron.qqe2.com/線上表示式生成器 66 triggerFactory.setCronExpression("? 0 0 * * ? *"); 67 68 return triggerFactory; 69 70 } 71 /** 72 * 建立排程工廠並返回 73 * @return 74 */ 75 @Bean("schedulerFactory") 76 public SchedulerFactoryBean createSchedulerFactory(){ 77 78 SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); 79 schedulerFactory.setTriggers(productSellDailyTriggerFactory.getObject()); 80 return schedulerFactory; 81 82 } 83 84 85 }
3:業務方法我就不貼了,大家可以列印一下測試一下即可(記得把cron表示式時間改小一點),譬如
1 package com.xqc.campusshop.service.impl; 2 3 import org.springframework.stereotype.Service; 4 5 @Service 6 public class ProductSellDailyServiceImpl implements ProductSellDailyService{ 7 8 @Override 9 public void dailyCalculate() { 10 system.out.println("Quartz跑起來了!”); 11 12 } 13 }
使用配置檔案方式
1:引入pom.xml(同上)
2:在Spring配置檔案配置如下
<!-- 使用MethodInvokingJobDetailFactoryBean,任務類可以不實現Job介面,通過targetMethod指定呼叫方法--> <bean id="productSellDailyService" class="com.xqc.campusshop.service"/> <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="group" value="job_product_sell_daily_group"/> <property name="name" value="product_sell_daily_job"/> <!--false表示等上一個任務執行完後再開啟新的任務--> <property name="concurrent" value="false"/> <property name="targetObject"> <ref bean="productSellDailyService"/> </property> <property name="targetMethod"> <value>dailyCalculate</value> </property> </bean> <!-- 排程觸發器 --> <bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="name" value="product_sell_daily_trigger"/> <property name="group" value="job_product_sell_daily_group"/> <property name="jobDetail"> <ref bean="jobDetail" /> </property> <property name="cronExpression"> <value>? 0 0 * * ? *</value> </property> </bean> <!-- 排程工廠 --> <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="myTrigger"/> </list> </property> </bean>
3:編寫業務方法(同上)
三:相關知識
三個核心概念:
Scheduler:排程器。所有的排程都是由它控制。
Trigger: 定義觸發的條件。
Job & JobDetail: JobDetail 定義的是任務資料,而真正的執行邏輯是在Job中,為什麼設計成 JobDetail + Job,不直接使用Job?這是因為任務是有可能併發執行,如果Scheduler直接使用Job,就會存在對同一個Job例項併發訪問的問題。而JobDetail & Job 方式,sheduler每次執行,都會根據JobDetail建立一個新的Job例項,這樣就可以規避併發訪問的問題。
核心關係圖
Quartz體系結構
重要組成部分
Job
Job例項在Quartz中的生命週期
每次排程器執行Job時,它在呼叫execute方法前會建立一個新的Job例項
當呼叫完成後,關聯的Job物件例項會被釋放,釋放例項會被垃圾回收機制回收。
JobBuild
JonDetail
JobDetail為Job例項提供了許多設定屬性,以及JobDataMap成員變數屬性,它用來儲存特定Job例項資訊,可以理解為Job攜帶的內容。排程器需要藉助JobDetail物件來新增Job例項
JobDetail和Trigger都有name和group。
name是它們在這個sheduler裡面的唯一標識。如果我們要更新一個JobDetail定義,只需要設定一個name相同的JobDetail例項即可。
group是一個組織單元,sheduler會提供一些對整組操作的API,比如 scheduler.resumeJobs()。
JobExecutionContext
當Scheduler呼叫一個Job,就會將JobExecutionContext傳遞給Job的execute()方法
Job能通過JobExecutionContext物件訪問到Quartz執行時候的環境以及Job本身的明細資料
JobDataMap
在進行任務排程時JobDataMap儲存,在JobExecutionContext中,非常方便獲取
JobDataMap可以用來裝載任何可序列化的資料物件,當Job例項物件被執行時這些引數物件會傳遞給他
JobDataMap實現了JDK的Map介面,並且添加了一些非常方便的方法用來存取基本的資料型別
獲取JobDataMap的兩種方式
從Map中直接獲取
Job實現類中新增Setter方法對應JobDataMap的鍵值(Quartz框架預設的JobFactory實現類在初始化Job例項物件時就會自動呼叫這些setter方法)
Trigger
startTime和endTime指定的Trigger會被觸發的時間區間。在這個區間之外,Trigger是不會被觸發的。
Trigger的實現類
SimpleTrigger
指定從某一個時間開始,以一定的時間間隔(單位是毫秒)執行的任務。
它適合的任務類似於:9:00 開始,每隔1小時,執行一次。
它的屬性有:
repeatInterval 重複間隔
repeatCount 重複次數。實際執行次數是 repeatCount+1。因為在startTime的時候一定會執行一次。
CronTrigger
適合於更復雜的任務,它支援型別於Linux Cron的語法(並且更強大)。基本上它覆蓋了以上三個Trigger的絕大部分能力(但不是全部)—— 當然,也更難理解。
它適合的任務類似於:每天0:00,9:00,18:00各執行一次。
它的屬性只有:
-
- Cron表示式。雖然有線上生成器,但是還是介紹一下
星號():可用在所有欄位中,表示對應時間域的每一個時刻,例如, 在分鐘欄位時,表示“每分鐘”;
問號(?):該字元只在日期和星期欄位中使用,它通常指定為“無意義的值”,相當於點位符;
減號(-):表達一個範圍,如在小時欄位中使用“10-12”,則表示從10到12點,即10,11,12;
逗號(,):表達一個列表值,如在星期欄位中使用“MON,WED,FRI”,則表示星期一,星期三和星期五;
斜槓(/):x/y表達一個等步長序列,x為起始值,y為增量步長值。如在分鐘欄位中使用0/15,則表示為0,15,30和45秒,而5/15在分鐘欄位中表示5,20,35,50,你也可以使用*/y,它等同於0/y;
L:該字元只在日期和星期欄位中使用,代表“Last”的意思,但它在兩個欄位中意思不同。L在日期欄位中,表示這個月份的最後一天,如一月的31號,非閏年二月的28號;如果L用在星期中,則表示星期六,等同於7。但是,如果L出現在星期欄位裡,而且在前面有一個數值X,則表示“這個月的最後X天”,例如,6L表示該月的最後星期五;
W:該字元只能出現在日期欄位裡,是對前導日期的修飾,表示離該日期最近的工作日。例如15W表示離該月15號最近的工作日,如果該月15號是星期六,則匹配14號星期五;如果15日是星期日,則匹配16號星期一;如果15號是星期二,那結果就是15號星期二。但必須注意關聯的匹配日期不能夠跨月,如你指定1W,如果1號是星期六,結果匹配的是3號星期一,而非上個月最後的那天。W字串只能指定單一日期,而不能指定日期範圍;
LW組合:在日期欄位可以組合使用LW,它的意思是當月的最後一個工作日;
井號(#):該字元只能在星期欄位中使用,表示當月某個工作日。如6#3表示當月的第三個星期五(6表示星期五,#3表示當前的第三個),而4#5表示當月的第五個星期三,假設當月沒有第五個星期三,忽略不觸發;
C:該字元只在日期和星期欄位中使用,代表“Calendar”的意思。它的意思是計劃所關聯的日期,如果日期沒有被關聯,則相當於日曆中所有日期。例如5C在日期欄位中就相當於日曆5日以後的第一天。1C在星期欄位中相當於星期日後的第一天。
JobStore
Quartz支援任務持久化,這可以讓你在執行時增加任務或者對現存的任務進行修改,併為後續任務的執行持久化這些變更和增加的部分。中心概念是JobStore介面。預設的是RAMJobStore。
ThreadTool
TriggerBuild
Scheduler
Calendar
Quartz體貼地為我們提供以下幾種Calendar,注意,所有的Calendar既可以是排除,也可以是包含,取決於:
HolidayCalendar。指定特定的日期,比如20140613。精度到天。
DailyCalendar。指定每天的時間段(rangeStartingTime, rangeEndingTime),格式是HH:MM[:SS[:mmm]]。也就是最大精度可以到毫秒。
WeeklyCalendar。指定每星期的星期幾,可選值比如為java.util.Calendar.SUNDAY。精度是天。
MonthlyCalendar。指定每月的幾號。可選值為1-31。精度是天
AnnualCalendar。 指定每年的哪一天。使用方式如上例。精度是天。
CronCalendar。指定Cron表示式。精度取決於Cron表示式,也就是最大精度可以到秒。
一個Trigger可以和多個Calendar關聯,以排除或包含某些時間點
監聽器:
JobListener,TriggerListener,SchedulerListener
原生執行過程:
通過工廠建立一個Scheduler
建立一個實現Job介面的實現類(就是要具體做的事情,可以具體呼叫自己寫的service)
定義一個Job,並繫結我們自己實現Job介面的實現類(例如通過JobBuilder的方式)
建立Trigger,並設定相關引數,如啟動時間等。
將job和trigger繫結到scheduler物件上,並啟動
程式碼略(網上一百度都有的HelloQuartz,我就懶得寫了)
四:深入探究
原理:
quartz定時排程是通過Object.wait方式(native方法)實現的,其本質是通過作業系統的時鐘來實現的。
設計模式:
Bulild模式
元件模式
Factory模式
鏈式寫法
執行緒檢視
在 Quartz 中,有兩類執行緒,Scheduler 排程執行緒和任務執行執行緒,其中任務執行執行緒通常使用一個執行緒池維護一組執行緒。
Scheduler 排程執行緒主要有兩個: 執行常規排程的執行緒,和執行 misfired trigger 的執行緒。常規排程執行緒輪詢儲存的所有 trigger,如果有需要觸發的 trigger,即到達了下一次觸發的時間,則從任務執行執行緒池獲取一個空閒執行緒,執行與該 trigger 關聯的任務。Misfire 執行緒是掃描所有的 trigger,檢視是否有 misfired trigger,如果有的話根據 misfire 的策略分別處理。下圖描述了這兩個執行緒的基本流程:
啟動流程
若quartz是配置在spring中,當伺服器啟動時,就會裝載相關的bean。SchedulerFactoryBean實現了InitializingBean介面,因此在初始化bean的時候,會執行afterPropertiesSet方法,該方法將會呼叫SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)建立Scheduler。SchedulerFactory在建立quartzScheduler的過程中,將會讀取配置引數,初始化各個元件
叢集配置
quartz叢集是通過資料庫表來感知其他的應用的,各個節點之間並沒有直接的通訊。只有使用持久的JobStore才能完成Quartz叢集。資料庫表:以前有12張表,現在只有11張表,現在沒有儲存listener相關的表,多了QRTZ_SIMPROP_TRIGGERS表:
QRTZ_LOCKS就是Quartz叢集實現同步機制的行鎖表,包括以下幾個鎖:CALENDAR_ACCESS 、JOB_ACCESS、MISFIRE_ACCESS 、STATE_ACCESS 、TRIGGER_ACCESS。
(篇幅有點長了,其他的以後再出吧!)