Spring中任务调度

任务调度

任务调度即在特定的时间点执行指定的操作。任务调度本身设计多线程并发,运行时间规则制定及解析,运行现场保持及恢复,线程池维护等。

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执行的时间触发规则。主要由两个子类接口:仅需要触发一次或以固定时间间隔执行时,适合选择SimpleTriggerCronTrigger适合复杂的调度方案,通过Cron表达式定义时间规则。

Calendar

org.quartz.Calendarjava.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比较简单,导入相关依赖,进行配置即可。(后续补充)

<bean name="quartzScheduler"
      class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="dataSource">
        <ref bean="myDataSource" /> //在这里指定数据源,配置中会失效
    </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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容