任务调度
任务调度即在特定的时间点执行指定的操作。任务调度本身设计多线程并发,运行时间规则制定及解析,运行现场保持及恢复,线程池维护等。
quartz是任务调度的成熟解决方案,功能强大使用简单。Spring提供了集成quartz的功能,也为JDK Timer 和 Excutor提供了支持。
在Spring中提供了一系列FactoryBean,可以很轻松的创建任务调度的实例;Spring还提供了几个工具类,可以将某个具体的Bean的方法作为被调度的任务;Spring还提供了支持线程池的执行调度器,它提供了一个抽象层,屏蔽了Java 1.3、JAVA 1.4、JAVA 1.5及Java EE之间的差异。
Quartz
Quartz允许开发人员灵活的定义触发器的调度时间表,并可对触发器和任务进行关联。Quartz提供了调度运行环境的持久化机制,可以保存并恢复调度现场。Quartz提供了监听器,各种插件,线程池等功能。(以下代码基于Quartz 1.8.6)
基础结构
Quartz对任务调度进行了高度的抽象,提出了调度器,任务和触发器三个核心概念,并在org.quartz
这个包中通过接口和类对核心概念进行描述。
Job
Job 是一个接口,内部只有一个方法。通过实现该接口定义需要执行的任务。JobExcutionContext提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。
public interface Job {
void execute(JobExecutionContext context)
throws JobExecutionException;
}
-
StatefulJob
Job的子接口,代表有状态的任务。该接口是个标签接口,让Quartz知道任务的类型,以便采取不同的执行方案。每个无状态的任务有自己的JobDataMap,所有有状态任务共享一个JobDataMap,StatefulJob每次任务执行对JobDataMap的更改会影响后续任务。所以有状态的StatefulJob不能并发执行,后续任务将阻塞等待直到本次任务执行完毕。除非必要,应避免StatefulJob的使用。
JobDetail
Quartz每次执行任务时,都根据接收的Job的实现类Class,通过反射创建一个Job实例,因此需要一个类对Job实现类和其他配置进行描述,如Job名称,描述,关联监听器等(类似于Spring中的BeanDefinition)。
public interface JobDetail extends Serializable, Cloneable {
public JobKey getKey();
public String getDescription();
public Class extends Job> getJobClass();
public JobDataMap getJobDataMap();
...
}
JobDataMap
JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。
将job加入到scheduler之前,在构建JobDetail时,可以将数据放入JobDataMap。
方式有两种
直接在构建JobDetail时通过JobBuilder的usingJobData方法将数据放入JobDataMap中。方法有两种:直接添加数据或者构造JobDataMap
//构造JobDataMap
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("jobData2", "hello 2");
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("helloJob","hello")
.usingJobData("jobData1","hello 1") //添加数据
.usingJobData(jobDataMap)
.build();
也可以在job类中,为JobDataMap中存储的数据的key增加set方法,那么Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样就不需要在execute()方法中显式地从map中取数据了。
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接口描述触发Job执行的时间触发规则。主要由两个子类接口:仅需要触发一次或以固定时间间隔执行时,适合选择SimpleTrigger
;CronTrigger
适合复杂的调度方案,通过Cron表达式定义时间规则。
Calendar
org.quartz.Calendar
和java.util.Calendar
不同,它是一些日历特定时间点的集合。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。例如每周一上午9点执行任务,如果遇到法定节假日不执行。针对不同的时间段类型,Quartz提供了不同的实现类,如AnnualCalendar
,HolidayCalendar
,MonthlyCalendar
等。
Scheduler
代表一个Quartz独立运行的容器,Trigger和JobDetail可以注册到Scheduler中,二者在Scheduler中各自拥有组和名称,组成了key。key是Scheduler查找定位容器中某一对象的依据,所以必须唯一(Trigger和JobDetail的key可以相同,因为类型不一样,处在不同的集合中)。
Scheduler定义了多个接口方法,允许外部通过组及名称对容器中的Trigger和JobDetail进行访问控制。
Scheduler可以将Trigger
绑定到某一个JobDetail
,这样当Trigger触发时,对应Job会被执行,一个Job可以对应多个Trigger,但是一个Trigger只能对应一个Job。
Scheduler实例通过SchedulerFactory
创建,Scheduler拥有一个SchedulerContext,SchedulerContext内部维护一个Map,以键值对形式保存上下文信息。job和Trigger都可以访问SchedulerContext内的信息。
Scheduler的生命周期
Scheduler的生命期, 从SchedulerFactory创建它时开始,到Scheduler调用shutdown()方法时结束;Scheduler被创建后,可以增加、删除和列举Job和Trigger,以及执行其它与调度相关的操作(如暂停Trigger)。但是,Scheduler只有在调用start()方法后,才会真正地触发trigger(即执行job)
JobBuilder
用于定义/构建JobDetail实例,用于定义作业的实例。
ThreadPool
Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程来提高运行效率。
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{
//构建SchedulerFactory实例
SchedulerFactory schedFact = new StdSchedulerFactory();
//获取Scheduler实例
Scheduler scheduler = schedFact.getScheduler();
//构建JobDetail实例
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("helloJob","hello")
.build();
//构建Trigger实例
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("helloTrigger","hello")
.startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
.build();
//将JobDetail实例和Trigger实例加入到调度容器
scheduler.scheduleJob(job,trigger);
//启动容器
scheduler.start();
}
Misfire策略
-
MISFIRE_INSTRUCTION_FIRE_NOW
设置方法:withMisfireHandlingInstructionFireNow
——以当前时间为触发频率立即触发执行
——执行至EndTIme的剩余周期次数
——以调度或恢复调度的时刻为基准的周期频率,EndTIme根据剩余次数和当前时间计算得到
——调整后的EndTIme会略大于根据StartTime计算的到的EndTIme值 -
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
设置方法:withMisfireHandlingInstructionIgnoreMisfires
含义:
——以错过的第一个频率时间立刻开始执行
——重做错过的所有频率周期
——当下一次触发频率发生时间大于当前时间以后,按照Interval的依次执行剩下的频率
——共执行RepeatCount+1次 -
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_CO
设置方法:withMisfireHandlingInstructionNowWithExistingCount
含义:
——以当前时间为触发频率立即触发执行
——执行至EndTIme的剩余周期次数
——以调度或恢复调度的时刻为基准的周期频率,EndTIme根据剩余次数和当前时间计算得到
——调整后的EndTIme会略大于根据StartTime计算的到的EndTIme值 -
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
设置方法:withMisfireHandlingInstructionNowWithRemainingCount
含义:
——以当前时间为触发频率立即触发执行
——执行至EndTIme的剩余周期次数
——以调度或恢复调度的时刻为基准的周期频率,EndTIme根据剩余次数和当前时间计算得到
——调整后的EndTIme会略大于根据StartTime计算的到的EndTIme值 -
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
设置方法:withMisfireHandlingInstructionNextWithRemainingCount
含义:
——不触发立即执行
——等待下次触发频率周期时刻,执行至EndTIme的剩余周期次数
——以StartTime为基准计算周期频率,并得到EndTIme
——即使中间出现pause,resume以后保持EndTIme时间不变 -
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
设置方法:withMisfireHandlingInstructionNextWithExistingCount
含义:
——此指令导致trigger忘记原始设置的StartTime和repeat-count
——触发器的repeat-count将被设置为剩余的次数
——这样会导致后面无法获得原始设定的StartTime和repeat-count值 -
默认策略
SimpleScheduleBuilder中misfireInstruction的默认值是MISFIRE_INSTRUCTION_SMART_POLICY,这是所有Trigger默认的MisFire策略,这个策略会根据Trigger的状态和类型来自动调节MisFire策略。
查看源码可以看到,若设置为默认策略,则按照以下规则来选择MisFire策略如果重复计数为0,则指令将解释为MISFIRE_INSTRUCTION_FIRE_NOW。
如果重复计数为REPEAT_INDEFINITELY,则指令将解释为MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT。 警告:如果触发器具有非空的结束时间,则使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT可能会导致触发器在失火时间范围内到达结束时,不会再次触发。
如果重复计数大于0,则指令将解释为MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT。
CronTrigger
CronTrigger能提供比SimpleTrigger更具有实际意义的调度方案,调度规则基于Cron表达式。
Demo
TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/30 * * * ? "))
.forJob("jobA", "groupA")
.build();
Misfire策略
-
MISFIRE_INSTRUCTION_DO_NOTHING
设置方法:withMisfireHandlingInstructionDoNothing
含义:
——不触发立即执行
——等待下次Cron触发频率到达时刻开始按照Cron频率依次执行 -
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
设置方法:withMisfireHandlingInstructionIgnoreMisfires
含义:
——以错过的第一个频率时间立刻开始执行
——重做错过的所有频率周期后
——当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行 -
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
设置方法:withMisfireHandlingInstructionFireAndProceed
含义:
——以当前时间为触发频率立刻触发一次执行
——然后按照Cron频率依次执行 -
默认策略
在SimpleTrigger中已经提到所有trigger的默认Misfire策略都是MISFIRE_INSTRUCTION_SMART_POLICY,SimpleTrigger会根据tirgger的状态来调整具体的Misfire策略,而CronTrigger的默认Misfire策略会被CronTrigger解释为MISFIRE_INSTRUCTION_FIRE_NOW,具体可以参照CronTrigger实现类的源码
Cron表达式
Cron([krɑn]):代表100万年,是英文中最大的时间单位。
Cron表达式对特殊字符的大小写不敏感。
时间 | 强制 | 允许值 | 允许的特殊字符 |
---|---|---|---|
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 | ,_*/ |
- * :可用在所有字段中,表示对应时间的每一时刻
- ?:在日期和星期中使用,通常表示无意义的值
- -:表达一个范围,在小时中
10-12
表示10点到12点,即10,11,12 - ,:表示一个列表值
- /:x/y表示一个等步长序列,x为起始值,y为增量步长值。如在分钟中
0/15
,表示0,15,30,45秒;也可以使用*/15
,等同于0/15
- L:再日期和星期中使用,代表"Last"。在日期中,代表最后一天,在星期中,代表星期六,等同于7(西方国家认为星期六是一周的最后一天)。例如6L在星期中代表最后一个星期五。
- W:只能出现在日期中,是对前导日期的修饰,表示离该日期最近的工作日。如
15W
,如果15日是周天,则匹配16号周一,如果15号是周六,则匹配14号周五。匹配不能跨月,只能指定单一时间。 - LW:在日期中使用,表示当月最后一天
- #:只能在星期中使用,表示当月某个工作日。6#7表示第7个星期五(6代表星期五,#7代表第7个),如果当月没有第七个星期5,则不触发。
- C:只能在日期和星期中使用,代表“Calendar”。意思计划所有关联的日期,如果日期没有被关联,则相当于日期中的所有日期。
Spring中使用Quartz
在Spring中使用Quartz比较简单,导入相关依赖,进行配置即可。(后续补充)
//在这里指定数据源,配置中会失效
#==============================================================
#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