任務(wù)調(diào)度
任務(wù)調(diào)度即在特定的時間點(diǎn)執(zhí)行指定的操作。任務(wù)調(diào)度本身設(shè)計多線程并發(fā),運(yùn)行時間規(guī)則制定及解析,運(yùn)行現(xiàn)場保持及恢復(fù),線程池維護(hù)等。
quartz是任務(wù)調(diào)度的成熟解決方案,功能強(qiáng)大使用簡單。Spring提供了集成quartz的功能,也為JDK Timer 和 Excutor提供了支持。
在Spring中提供了一系列FactoryBean,可以很輕松的創(chuàng)建任務(wù)調(diào)度的實例;Spring還提供了幾個工具類,可以將某個具體的Bean的方法作為被調(diào)度的任務(wù);Spring還提供了支持線程池的執(zhí)行調(diào)度器,它提供了一個抽象層,屏蔽了Java 1.3、JAVA 1.4、JAVA 1.5及Java EE之間的差異。
Quartz
Quartz允許開發(fā)人員靈活的定義觸發(fā)器的調(diào)度時間表,并可對觸發(fā)器和任務(wù)進(jìn)行關(guān)聯(lián)。Quartz提供了調(diào)度運(yùn)行環(huán)境的持久化機(jī)制,可以保存并恢復(fù)調(diào)度現(xiàn)場。Quartz提供了監(jiān)聽器,各種插件,線程池等功能。(以下代碼基于Quartz 1.8.6)
基礎(chǔ)結(jié)構(gòu)
Quartz對任務(wù)調(diào)度進(jìn)行了高度的抽象,提出了調(diào)度器,任務(wù)和觸發(fā)器三個核心概念,并在org.quartz這個包中通過接口和類對核心概念進(jìn)行描述。
Job
Job 是一個接口,內(nèi)部只有一個方法。通過實現(xiàn)該接口定義需要執(zhí)行的任務(wù)。JobExcutionContext提供了調(diào)度上下文的各種信息。Job運(yùn)行時的信息保存在JobDataMap實例中。
public interface Job {
void execute(JobExecutionContext context)
throws JobExecutionException;
}
-
StatefulJob
Job的子接口,代表有狀態(tài)的任務(wù)。該接口是個標(biāo)簽接口,讓Quartz知道任務(wù)的類型,以便采取不同的執(zhí)行方案。每個無狀態(tài)的任務(wù)有自己的JobDataMap,所有有狀態(tài)任務(wù)共享一個JobDataMap,StatefulJob每次任務(wù)執(zhí)行對JobDataMap的更改會影響后續(xù)任務(wù)。所以有狀態(tài)的StatefulJob不能并發(fā)執(zhí)行,后續(xù)任務(wù)將阻塞等待直到本次任務(wù)執(zhí)行完畢。除非必要,應(yīng)避免StatefulJob的使用。
JobDetail
Quartz每次執(zhí)行任務(wù)時,都根據(jù)接收的Job的實現(xiàn)類Class,通過反射創(chuàng)建一個Job實例,因此需要一個類對Job實現(xiàn)類和其他配置進(jìn)行描述,如Job名稱,描述,關(guān)聯(lián)監(jiān)聽器等(類似于Spring中的BeanDefinition)。
public interface JobDetail extends Serializable, Cloneable {
public JobKey getKey();
public String getDescription();
public Class<? extends Job> getJobClass();
public JobDataMap getJobDataMap();
...
}
JobDataMap
JobDataMap中可以包含不限量的(序列化的)數(shù)據(jù)對象,在job實例執(zhí)行的時候,可以使用其中的數(shù)據(jù);JobDataMap是Java Map接口的一個實現(xiàn),額外增加了一些便于存取基本類型的數(shù)據(jù)的方法。
將job加入到scheduler之前,在構(gòu)建JobDetail時,可以將數(shù)據(jù)放入JobDataMap。
方式有兩種
直接在構(gòu)建JobDetail時通過JobBuilder的usingJobData方法將數(shù)據(jù)放入JobDataMap中。方法有兩種:直接添加數(shù)據(jù)或者構(gòu)造JobDataMap
//構(gòu)造JobDataMap
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("jobData2", "hello 2");
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("helloJob","hello")
.usingJobData("jobData1","hello 1") //添加數(shù)據(jù)
.usingJobData(jobDataMap)
.build();
也可以在job類中,為JobDataMap中存儲的數(shù)據(jù)的key增加set方法,那么Quartz的默認(rèn)JobFactory實現(xiàn)在job被實例化的時候會自動調(diào)用這些set方法,這樣就不需要在execute()方法中顯式地從map中取數(shù)據(jù)了。
public class SimpleJob implements Job {
private String jobData1;
private String jobData2;
public void setJobData1(String jobData1) {
this.jobData1 = jobData1;
}
public void setJobData2(String jobData2) {
this.jobData2 = jobData2;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// System.out.println(context.getJobDetail().getJobDataMap().get("jobData1"));
// System.out.println(context.getJobDetail().getJobDataMap().get("jobData2"));
System.out.println(jobData1);
System.out.println(jobData2);
System.out.println("hello,quartz!"+ context.getJobDetail().getKey()+":::"+ context.getTrigger().getKey());
}
}
Trigger
Trigger接口描述觸發(fā)Job執(zhí)行的時間觸發(fā)規(guī)則。主要由兩個子類接口:僅需要觸發(fā)一次或以固定時間間隔執(zhí)行時,適合選擇SimpleTrigger;CronTrigger適合復(fù)雜的調(diào)度方案,通過Cron表達(dá)式定義時間規(guī)則。
Calendar
org.quartz.Calendar和java.util.Calendar不同,它是一些日歷特定時間點(diǎn)的集合。一個Trigger可以和多個Calendar關(guān)聯(lián),以便排除或包含某些時間點(diǎn)。例如每周一上午9點(diǎn)執(zhí)行任務(wù),如果遇到法定節(jié)假日不執(zhí)行。針對不同的時間段類型,Quartz提供了不同的實現(xiàn)類,如AnnualCalendar,HolidayCalendar,MonthlyCalendar等。
Scheduler
代表一個Quartz獨(dú)立運(yùn)行的容器,Trigger和JobDetail可以注冊到Scheduler中,二者在Scheduler中各自擁有組和名稱,組成了key。key是Scheduler查找定位容器中某一對象的依據(jù),所以必須唯一(Trigger和JobDetail的key可以相同,因為類型不一樣,處在不同的集合中)。
Scheduler定義了多個接口方法,允許外部通過組及名稱對容器中的Trigger和JobDetail進(jìn)行訪問控制。
Scheduler可以將Trigger綁定到某一個JobDetail,這樣當(dāng)Trigger觸發(fā)時,對應(yīng)Job會被執(zhí)行,一個Job可以對應(yīng)多個Trigger,但是一個Trigger只能對應(yīng)一個Job。
Scheduler實例通過SchedulerFactory創(chuàng)建,Scheduler擁有一個SchedulerContext,SchedulerContext內(nèi)部維護(hù)一個Map,以鍵值對形式保存上下文信息。job和Trigger都可以訪問SchedulerContext內(nèi)的信息。
Scheduler的生命周期
Scheduler的生命期, 從SchedulerFactory創(chuàng)建它時開始,到Scheduler調(diào)用shutdown()方法時結(jié)束;Scheduler被創(chuàng)建后,可以增加、刪除和列舉Job和Trigger,以及執(zhí)行其它與調(diào)度相關(guān)的操作(如暫停Trigger)。但是,Scheduler只有在調(diào)用start()方法后,才會真正地觸發(fā)trigger(即執(zhí)行job)
JobBuilder
用于定義/構(gòu)建JobDetail實例,用于定義作業(yè)的實例。
ThreadPool
Scheduler使用一個線程池作為任務(wù)運(yùn)行的基礎(chǔ)設(shè)施,任務(wù)通過共享線程池中的線程來提高運(yùn)行效率。
SimpleTrigger
Demo
public class SimpleJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("hello,quartz!"+ context.getJobDetail().getKey()+":::"+ context.getTrigger().getKey());
}
}
public static void main(String[] args) throws Exception{
//構(gòu)建SchedulerFactory實例
SchedulerFactory schedFact = new StdSchedulerFactory();
//獲取Scheduler實例
Scheduler scheduler = schedFact.getScheduler();
//構(gòu)建JobDetail實例
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("helloJob","hello")
.build();
//構(gòu)建Trigger實例
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("helloTrigger","hello")
.startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
.build();
//將JobDetail實例和Trigger實例加入到調(diào)度容器
scheduler.scheduleJob(job,trigger);
//啟動容器
scheduler.start();
}
Misfire策略
-
MISFIRE_INSTRUCTION_FIRE_NOW
設(shè)置方法:withMisfireHandlingInstructionFireNow
——以當(dāng)前時間為觸發(fā)頻率立即觸發(fā)執(zhí)行
——執(zhí)行至EndTIme的剩余周期次數(shù)
——以調(diào)度或恢復(fù)調(diào)度的時刻為基準(zhǔn)的周期頻率,EndTIme根據(jù)剩余次數(shù)和當(dāng)前時間計算得到
——調(diào)整后的EndTIme會略大于根據(jù)StartTime計算的到的EndTIme值 -
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
設(shè)置方法:withMisfireHandlingInstructionIgnoreMisfires
含義:
——以錯過的第一個頻率時間立刻開始執(zhí)行
——重做錯過的所有頻率周期
——當(dāng)下一次觸發(fā)頻率發(fā)生時間大于當(dāng)前時間以后,按照Interval的依次執(zhí)行剩下的頻率
——共執(zhí)行RepeatCount+1次 -
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_CO
設(shè)置方法:withMisfireHandlingInstructionNowWithExistingCount
含義:
——以當(dāng)前時間為觸發(fā)頻率立即觸發(fā)執(zhí)行
——執(zhí)行至EndTIme的剩余周期次數(shù)
——以調(diào)度或恢復(fù)調(diào)度的時刻為基準(zhǔn)的周期頻率,EndTIme根據(jù)剩余次數(shù)和當(dāng)前時間計算得到
——調(diào)整后的EndTIme會略大于根據(jù)StartTime計算的到的EndTIme值 -
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
設(shè)置方法:withMisfireHandlingInstructionNowWithRemainingCount
含義:
——以當(dāng)前時間為觸發(fā)頻率立即觸發(fā)執(zhí)行
——執(zhí)行至EndTIme的剩余周期次數(shù)
——以調(diào)度或恢復(fù)調(diào)度的時刻為基準(zhǔn)的周期頻率,EndTIme根據(jù)剩余次數(shù)和當(dāng)前時間計算得到
——調(diào)整后的EndTIme會略大于根據(jù)StartTime計算的到的EndTIme值 -
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
設(shè)置方法:withMisfireHandlingInstructionNextWithRemainingCount
含義:
——不觸發(fā)立即執(zhí)行
——等待下次觸發(fā)頻率周期時刻,執(zhí)行至EndTIme的剩余周期次數(shù)
——以StartTime為基準(zhǔn)計算周期頻率,并得到EndTIme
——即使中間出現(xiàn)pause,resume以后保持EndTIme時間不變 -
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
設(shè)置方法:withMisfireHandlingInstructionNextWithExistingCount
含義:
——此指令導(dǎo)致trigger忘記原始設(shè)置的StartTime和repeat-count
——觸發(fā)器的repeat-count將被設(shè)置為剩余的次數(shù)
——這樣會導(dǎo)致后面無法獲得原始設(shè)定的StartTime和repeat-count值 -
默認(rèn)策略
SimpleScheduleBuilder中misfireInstruction的默認(rèn)值是MISFIRE_INSTRUCTION_SMART_POLICY,這是所有Trigger默認(rèn)的MisFire策略,這個策略會根據(jù)Trigger的狀態(tài)和類型來自動調(diào)節(jié)MisFire策略。
查看源碼可以看到,若設(shè)置為默認(rèn)策略,則按照以下規(guī)則來選擇MisFire策略如果重復(fù)計數(shù)為0,則指令將解釋為MISFIRE_INSTRUCTION_FIRE_NOW。
如果重復(fù)計數(shù)為REPEAT_INDEFINITELY,則指令將解釋為MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT。 警告:如果觸發(fā)器具有非空的結(jié)束時間,則使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT可能會導(dǎo)致觸發(fā)器在失火時間范圍內(nèi)到達(dá)結(jié)束時,不會再次觸發(fā)。
如果重復(fù)計數(shù)大于0,則指令將解釋為MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT。
CronTrigger
CronTrigger能提供比SimpleTrigger更具有實際意義的調(diào)度方案,調(diào)度規(guī)則基于Cron表達(dá)式。
Demo
TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/30 * * * ? "))
.forJob("jobA", "groupA")
.build();
Misfire策略
-
MISFIRE_INSTRUCTION_DO_NOTHING
設(shè)置方法:withMisfireHandlingInstructionDoNothing
含義:
——不觸發(fā)立即執(zhí)行
——等待下次Cron觸發(fā)頻率到達(dá)時刻開始按照Cron頻率依次執(zhí)行 -
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
設(shè)置方法:withMisfireHandlingInstructionIgnoreMisfires
含義:
——以錯過的第一個頻率時間立刻開始執(zhí)行
——重做錯過的所有頻率周期后
——當(dāng)下一次觸發(fā)頻率發(fā)生時間大于當(dāng)前時間后,再按照正常的Cron頻率依次執(zhí)行 -
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
設(shè)置方法:withMisfireHandlingInstructionFireAndProceed
含義:
——以當(dāng)前時間為觸發(fā)頻率立刻觸發(fā)一次執(zhí)行
——然后按照Cron頻率依次執(zhí)行 -
默認(rèn)策略
在SimpleTrigger中已經(jīng)提到所有trigger的默認(rèn)Misfire策略都是MISFIRE_INSTRUCTION_SMART_POLICY,SimpleTrigger會根據(jù)tirgger的狀態(tài)來調(diào)整具體的Misfire策略,而CronTrigger的默認(rèn)Misfire策略會被CronTrigger解釋為MISFIRE_INSTRUCTION_FIRE_NOW,具體可以參照CronTrigger實現(xiàn)類的源碼
Cron表達(dá)式
Cron([krɑn]):代表100萬年,是英文中最大的時間單位。
Cron表達(dá)式對特殊字符的大小寫不敏感。
| 時間 | 強(qiáng)制 | 允許值 | 允許的特殊字符 |
|---|---|---|---|
| Seconds | yes | 0-59 | ,_*/ |
| Minutes | yes | 0-59 | ,_*/ |
| Hours | yes | 0-23 | ,_*/ |
| Day Of month | yes | 1-31 | ,_*?/LW |
| Month | yes | 1-12 or JAX-DEC | ,_*/ |
| Day of Week | yes | 1-7 or SUN-SAT | ,_*?/L# |
| Year | no | empty,1970-2099 | ,_*/ |
- * :可用在所有字段中,表示對應(yīng)時間的每一時刻
- ?:在日期和星期中使用,通常表示無意義的值
- -:表達(dá)一個范圍,在小時中
10-12表示10點(diǎn)到12點(diǎn),即10,11,12 - ,:表示一個列表值
- /:x/y表示一個等步長序列,x為起始值,y為增量步長值。如在分鐘中
0/15,表示0,15,30,45秒;也可以使用*/15,等同于0/15 - L:再日期和星期中使用,代表"Last"。在日期中,代表最后一天,在星期中,代表星期六,等同于7(西方國家認(rèn)為星期六是一周的最后一天)。例如6L在星期中代表最后一個星期五。
- W:只能出現(xiàn)在日期中,是對前導(dǎo)日期的修飾,表示離該日期最近的工作日。如
15W,如果15日是周天,則匹配16號周一,如果15號是周六,則匹配14號周五。匹配不能跨月,只能指定單一時間。 - LW:在日期中使用,表示當(dāng)月最后一天
- #:只能在星期中使用,表示當(dāng)月某個工作日。6#7表示第7個星期五(6代表星期五,#7代表第7個),如果當(dāng)月沒有第七個星期5,則不觸發(fā)。
- C:只能在日期和星期中使用,代表“Calendar”。意思計劃所有關(guān)聯(lián)的日期,如果日期沒有被關(guān)聯(lián),則相當(dāng)于日期中的所有日期。
Spring中使用Quartz
在Spring中使用Quartz比較簡單,導(dǎo)入相關(guān)依賴,進(jìn)行配置即可。(后續(xù)補(bǔ)充)
<bean name="quartzScheduler"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="dataSource">
<ref bean="myDataSource" /> //在這里指定數(shù)據(jù)源,配置中會失效
</property>
<!-- 指定Spring容器 -->
<property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
<property name="configLocation" value="classpath:quartz.properties" />
</bean>
#==============================================================
#Configure Main Scheduler Properties
#==============================================================
org.quartz.scheduler.instanceName = quartzScheduler
org.quartz.scheduler.instanceId = AUTO
#==============================================================
#Configure JobStore
#==============================================================
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
#org.quartz.jobStore.dataSource = myDS
#==============================================================
#Configure DataSource
#==============================================================
#org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
#org.quartz.dataSource.myDS.URL =jdbc:mysql://localhost:3306/quartz_db?useUnicode=true&characterEncoding=UTF-8
#org.quartz.dataSource.myDS.user = root
#org.quartz.dataSource.myDS.password = 123456
#org.quartz.dataSource.myDS.maxConnections = 30
#==============================================================
#Configure ThreadPool
#==============================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 100
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true