转自:http://www.cnblogs.com/bingoidea/archive/2009/08/05/1539656.htmlhtml
上一篇:定时器的实现、Java定时器Timer和Quartz介绍与spring中定时器的配置http://blog.csdn.NET/sup_heaven/article/details/37738255java
概述mysql
各类企业应用几乎都会碰到任务调度的需求,就拿论坛来讲:每隔半个小时生成精华文章的RSS文件,天天凌晨统计论坛用户的积分排名,每隔30分钟执行锁定用户解锁任务。linux
对于一个典型的MIS系统来讲,在每个月1号凌晨统计上个月各部门的业务数据生成月报表,每半个小时查询用户是否已经有快到期的待处理业务……,这样的例子俯拾皆是,不胜枚举。web
任务调度自己涉及到多线程并发、运行时间规则制定和解析、场景保持与恢复、线程池维护等诸多方面的工做。若是直接使用自定义线程这种刀耕火种的原始办法,开发任务调度程序是一项颇具挑战性的工做。Java开源的好处就是:领域问题都能找到现成的解决方案。spring
OpenSymphony所提供的Quartz自2001年发布版本以来已经被众多项目做为任务调度的解决方案,Quartz在提供巨大灵活性的同时并未牺牲其简单性,它所提供的强大功能使你能够应付绝大多数的调度需求。sql
Quartz 在开源任务调度框架中的翘首,它提供了强大任务调度机制,难能难得的是它同时保持了使用的简单性。Quartz 容许开发人员灵活地定义触发器的调度时间表,并能够对触发器和任务进行关联映射。数据库
此外,Quartz提供了调度运行环境的持久化机制,能够保存并恢复调度现场,即便系统因故障关闭,任务调度现场数据并不会丢失。此外,Quartz还提供了组件式的侦听器、各类插件、线程池等功能。服务器
了解Quartz体系结构多线程
Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个核心的概念,并在org.quartz经过接口和类对重要的这些核心概念进行描述:
●Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各类信息。Job运行时的信息保存在JobExecutionContext实例中;
●JobDetail:Quartz在每次执行Job时,都从新建立一个Job实例,因此它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时经过newInstance()的反射机制实例化Job。所以须要经过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。
经过该类的构造函数能够更具体地了解它的功用:JobDetail(java.lang.String name, java.lang.String group, java.lang.Class jobClass),该构造函数要求指定Job的实现类,以及任务在Scheduler中的组名和Job名称;
●Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则能够经过Cron表达式定义出各类复杂时间规则的调度方案:如每早晨9:00执行,周1、周3、周五下午5:00执行等;
●Calendar:org.quartz.Calendar和java.util.Calendar不一样,它是一些日历特定时间点的集合(能够简单地将org.quartz.Calendar看做java.util.Calendar的集合——java.util.Calendar表明一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger能够和多个Calendar关联,以便排除或包含某些时间点。
假设,咱们安排每周星期一早上10:00执行任务,可是若是碰到法定的节日,任务则不执行,这时就须要在Trigger触发机制的基础上使用Calendar进行定点排除。针对不一样时间段类型,Quartz在org.quartz.impl.calendar包下提供了若干个Calendar的实现类,如AnnualCalendar、MonthlyCalendar、WeeklyCalendar分别针对每一年、每个月和每周进行定义;
●Scheduler:表明一个Quartz的独立运行容器,Trigger和JobDetail能够注册到Scheduler中,二者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须惟一,JobDetail的组和名称也必须惟一(但能够和Trigger的组和名称相同,由于它们是不一样类型的)。Scheduler定义了多个接口方法,容许外部经过组及名称访问和控制容器中Trigger和JobDetail。
Scheduler能够将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job能够对应多个Trigger,但一个Trigger只能对应一个Job。能够经过SchedulerFactory建立一个Scheduler实例。Scheduler拥有一个SchedulerContext,它相似于ServletContext,保存着Scheduler上下文信息,Job和Trigger均可以访问SchedulerContext内的信息。SchedulerContext内部经过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。能够经过Scheduler# getContext()获取对应的SchedulerContext实例;
●ThreadPool:Scheduler使用一个线程池做为任务运行的基础设施,任务经过共享线程池中的线程提升运行效率。
Job有一个StatefulJob子接口,表明有状态的任务,该接口是一个没有方法的标签接口,其目的是让Quartz知道任务的类型,以便采用不一样的执行方案。无状态任务在执行时拥有本身的JobDataMap拷贝,对JobDataMap的更改不会影响下次的执行。而有状态任务共享共享同一个JobDataMap实例,每次任务执行对JobDataMap所作的更改会保存下来,后面的执行能够看到这个更改,也即每次执行任务后都会对后面的执行发生影响。
正由于这个缘由,无状态的Job能够并发执行,而有状态的StatefulJob不能并发执行,这意味着若是前次的StatefulJob尚未执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务须要考虑更多的因素,程序每每拥有更高的复杂度,所以除非必要,应该尽可能使用无状态的Job。
若是Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。
Trigger自身也能够拥有一个JobDataMap,其关联的Job能够经过JobExecutionContext#getTrigger().getJobDataMap()获取Trigger中的JobDataMap(该JobDataMap和JobDetail所拥有的JobDataMap不是同一个哦)。无论是有状态仍是无状态的任务,在任务执行期间对Trigger的JobDataMap所作的更改都不会进行持久,也即不会对下次的执行产生影响。
Quartz拥有完善的事件和监听体系,大部分组件都拥有事件,如任务执行前事件、任务执行后事件、触发器触发前事件、触发后事件、调度器开始事件、关闭事件等等,能够注册相应的监听器处理感兴趣的事件。
图1描述了Scheduler的内部组件结构,SchedulerContext提供Scheduler全局可见的上下文信息,每个任务都对应一个JobDataMap,虚线表达的JobDataMap表示对应有状态的任务:
Scheduler结构图
一个Scheduler能够拥有多个Triger组和多个JobDetail组,注册Trigger和JobDetail时,若是不显式指定所属的组,Scheduler将放入到默认组中,默认组的组名为Scheduler.DEFAULT_GROUP。组名和名称组成了对象的全名,同一类型对象的全名不能相同。
Scheduler自己就是一个容器,它维护着Quartz的各类组件并实施调度的规则。Scheduler还拥有一个线程池,线程池为任务提供执行线程——这比执行任务时简单地建立一个新线程要拥有更高的效率,同时经过共享节约资源的占用。经过线程池组件的支持,对于繁忙度高、压力大的任务调度,Quartz将能够提供良好的伸缩性。
提示: Quartz完整下载包examples目录下拥有10多个实例,它们是快速掌握Quartz应用很好的实例。
使用SimpleTrigger
SimpleTrigger拥有多个重载的构造函数,用以在不一样场合下构造出对应的实例:
●SimpleTrigger(String name, String group):经过该构造函数指定Trigger所属组和名称;
●SimpleTrigger(String name, String group, Date startTime):除指定Trigger所属组和名称外,还能够指定触发的开发时间;
●SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval):除指定以上信息外,还能够指定结束时间、重复执行次数、时间间隔等参数;
●SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval):这是最复杂的一个构造函数,在指定触发参数的同时,还经过jobGroup和jobName,让该Trigger和Scheduler中的某个任务关联起来。
Cron表达式
Quartz使用相似于Linux下的Cron表达式定义时间规则,Cron表达式由6或7个由空格分隔的时间字段组成,如表1所示:
Cron表达式时间字段
位置 |
时间域名 |
容许值 |
容许的特殊字符 |
1 |
秒 |
0-59 |
, - * / |
2 |
分钟 |
0-59 |
, - * / |
3 |
小时 |
0-23 |
, - * / |
4 |
日期 |
1-31 |
, - * ? / L W C |
5 |
月份 |
1-12 |
, - * / |
6 |
星期 |
1-7 |
, - * ? / L C # |
7 |
年(可选) |
空值1970-2099 |
, - * / |
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在星期字段中至关于星期往后的第一天。
Cron表达式对特殊字符的大小写不敏感,对表明星期的缩写英文大小写也不敏感。
Cron表示式示例
表示式 |
说明 |
"0 0 12 * * ? " |
天天12点运行 |
"0 15 10 ? * *" |
天天10:15运行 |
"0 15 10 * * ?" |
天天10:15运行 |
"0 15 10 * * ? *" |
天天10:15运行 |
"0 15 10 * * ? 2008" |
在2008年的天天10:15运行 |
"0 * 14 * * ?" |
天天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。 |
"0 0/5 14 * * ?" |
天天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。 |
"0 0/5 14,18 * * ?" |
天天14点到15点每5分钟运行一次,此外天天18点到19点每5钟也运行一次。 |
"0 0-5 14 * * ?" |
天天14:00点到14:05,每分钟运行一次。 |
"0 10,44 14 ? 3 WED" |
3月每周三的14:10分到14:44,每分钟运行一次。 |
"0 15 10 ? * MON-FRI" |
每周一,二,三,四,五的10:15分运行。 |
"0 15 10 15 * ?" |
每个月15日10:15分运行。 |
"0 15 10 L * ?" |
每个月最后一天10:15分运行。 |
"0 15 10 ? * 6L" |
每个月最后一个星期五10:15分运行。 |
"0 15 10 ? * 6L 2007-2009" |
在2007,2008,2009年每月的最后一个星期五的10:15分运行。 |
"0 15 10 ? * 6#3" |
每个月第三个星期五的10:15分运行。 |
代码示例
任务调度信息存储
在默认状况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,由于内存中数据访问最快。不足之处是缺少数据的持久性,当程序路途中止或系统崩溃时,全部运行的信息都会丢失。好比咱们但愿安排一个执行100次的任务,若是执行到50次时系统崩溃了,系统重启时任务的执行计数器将从0开始。在大多数实际的应用中,咱们每每并不须要保存任务调度的现场数据,由于不多须要规划一个指定执行次数的任务。
对于仅执行一次的任务来讲,其执行条件信息自己应该是已经持久化的业务数据(如锁定到期解锁任务,解锁的时间应该是业务数据),当执行完成后,条件信息也会相应改变。固然调度现场信息不只仅是记录运行次数,还包括调度规则、JobDataMap中的数据等等。
若是确实须要持久化任务调度信息,Quartz容许你经过调整其属性文件,将这些信息保存到数据库中。使用数据库保存任务调度信息后,即便系统崩溃后从新启动,任务的调度信息将获得恢复。如前面所说的例子,执行50次崩溃后从新运行,计数器将从51开始计数。使用了数据库保存信息的任务称为持久化任务。
经过配置文件调整任务调度信息的保存策略
其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件并提供了默认设置。若是须要调整默认配置,能够在类路径下创建一个新的quartz.properties,它将自动被Quartz加载并覆盖默认的设置。
先来了解一下Quartz的默认属性配置文件:
quartz.properties:默认配置
Quartz的属性配置文件主要包括三方面的信息:
1)集群信息;
2)调度器线程池;
3)任务调度现场数据的保存。
若是任务数目很大时,能够经过增大线程池的大小获得更好的性能。默认状况下,Quartz采用org.quartz.simpl.RAMJobStore保存任务的现场数据,顾名思义,信息保存在RAM内存中,咱们能够经过如下设置将任务调度现场数据保存到数据库中:
quartz.properties:使用数据库保存任务调度现场数据
要将任务调度数据保存到数据库中,就必须使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原来的org.quartz.simpl.RAMJobStore并提供相应的数据库配置信息。首先①处指定了Quartz数据库表的前缀,在②处定义了一个数据源,在③处具体定义这个数据源的链接信息。
你必须事先在相应的数据库中建立Quartz的数据表(共8张),在Quartz的完整发布包的docs/dbTables目录下拥有对应不一样数据库的SQL脚本。
查询数据库中的运行信息
任务的现场保存对于上层的Quartz程序来讲是彻底透明的,咱们在src目录下编写一个如上代码所示的quartz.properties文件后,从新运行以下代码示例的程序,在数据库表中将能够看到对应的持久化信息。当调度程序运行过程当中途中止后,任务调度的现场数据将记录在数据表中,在系统重启时就能够在此基础上继续进行任务的调度。
当代码SimpleTriggerRunner执行到一段时间后非正常退出,咱们就能够经过这个JDBCJobStoreRunner根据记录在数据库中的现场数据恢复任务的调度。Scheduler中的全部Trigger以及JobDetail的运行信息都会保存在数据库中,这里咱们仅恢复tgroup1组中名称为trigger1_1的触发器,这能够经过所示的代码进行过滤,触发器的采用GROUP.TRIGGER_NAME的全名格式。经过Scheduler#rescheduleJob(String triggerName,String groupName,Trigger newTrigger)便可从新调度关联某个Trigger的任务。
下面咱们来观察一下不一样时期qrtz_simple_triggers表的数据:
1.运行代码SimpleTriggerRunner一小段时间后退出:
REPEAT_COUNT表示须要运行的总次数,而TIMES_TRIGGER表示已经运行的次数。
2.运行代码JDBCJobStoreRunner恢复trigger1_1的触发器,运行一段时间后退出,这时qrtz_simple_triggers中的数据以下:
首先Quartz会将原REPEAT_COUNT-TIMES_TRIGGER获得新的REPEAT_COUNT值,并记录已经运行的次数(从新从0开始计算)。
3.从新启动JDBCJobStoreRunner运行后,数据又将发生相应的变化:
4.继续运行直至完成全部剩余的次数,再次查询qrtz_simple_triggers表:
这时,该表中的记录已经变空。
值得注意的是,若是你使用JDBC保存任务调度数据时,当你运行代码SimpleTriggerRunner而后退出,当再次但愿运行SimpleTriggerRunner时,系统将抛出JobDetail重名的异常:Unable to store Job with name: 'job1_1' and group: 'jGroup1', because one already exists with this identification.
由于每次调用Scheduler#scheduleJob()时,Quartz都会将JobDetail和Trigger的信息保存到数据库中,若是数据表中已经同名的JobDetail或Trigger,异常就产生了。
本文使用quartz 1.6版本,咱们发现当后台数据库使用MySQL时,数据保存不成功,该错误是Quartz的一个Bug,相信会在高版本中获得修复。由于HSQLDB不支持SELECT * FROM TABLE_NAME FOR UPDATE的语法,因此不能使用HSQLDB数据库。
附录
·org.quartz.scheduler.instanceName
每一个 Scheduler 必须给定一个名称来标识。当在同一个程序中有多个实例时,这个名称做为客户代码识别是哪一个 Scheduler 而用。假如你用到了集群特性,你就必须为集群中的每个实例使用相同的名称,以使它们成为“逻辑上” 是同一个 Scheduler 。
·org.quartz.scheduler.instanceId
每一个 Quartz Scheduler 必须指定一个惟一的 ID。这个值能够是任何字符串值,只要对于全部的 Scheduler 是惟一的。若是你想要自动生成的 ID,那你可使用AUTO 做为 instanceId 。从版本 1.5.1 开始,你可以定制如何自动生成实例 ID。见instanceIDGenerator.class 属性,会在接下来说到。
·org.quartz.scheduler.instanceIdGenerator.class
从版本 1.5.1 开始,这个属性容许你定制instanceId 的生成,这个属性仅被用于属性org.quartz.scheduler.instanceId 设置为AUTO 的状况下。默认是 org.quartz.simpl.SimpleInstanceIdGenerator ,它会基于主机名和时间戳来产生实例 ID 的。
·org.quartz.scheduler.threadName
能够是对于 Java 线程来讲有效名称的任何字符串。假如这个属性未予指定,线程将会接受 Scheduler 名称 (org.quartz.scheduler.instanceName ) 前附加上字符串 '_QuartzSchedulerThread' 做为名称。
·org.quartz.scheduler.idelWaitTime
这个属性设置了当 Scheduler 处于空闲时转而再次查询可用 Trigger 时所等待的毫秒数。一般,你无需调整这个参数,除非你正使用 XA 事物,遇到了 Trigger 本该当即触发而发生延迟的问题。
·org.quartz.scheduler.dbFailureRetryInterval
这个属性设置 Scheduler 在检测到 JobStore 到某处的链接(好比到数据库的链接) 断开后,再次尝试链接所等待的毫秒数。这个参数在使用 RamJobStore 无效。
·org.quartz.scheduler.classLoadHelper.class
对于多数健状的应用,所使用的默认值为 org.quartz.simpl.CascadingClassLoadHelper 类,它会依序使用其余的ClassLoadHelper 类,直到有一个能正常工做为止。你大概没必须为这个属性指定任何其余的类,除非有可能在应用服务器中时。当前全部可能的ClassLoadHelper 实现可在 org.quartz.simpl 包中找到。
·org.quartz.context.key.SOME_KEY
这个属性用于向 "Scheduler 上下文" 中置入一个 名-值 对表示的字符串值。(见 Scheduler.getContext() )。所以,好比设置了org.quartz.context.key.MyEmail = myemail@somehost.com就至关于执行了scheduler.getContext().put("MyEmail", myemail@somehost.com)
·org.quartz.scheduler.userTransactionURL
它设置了 Quartz 能在哪里定位到应用服务器的 UserTransaction 管理器的 JNDI URL。默认值(未设定的话) 是java:comp/UserTransaction ,这几乎能工做于全部的应用服务器中。Websphere 用户也许须要设置这个属性为jta/usertransaction 。这个属性仅用于 Quartz 配置使用 JobStoreCMT 的状况,而且org.quartz.scheduler.wrapJobExecutionInUserTransaction 被设定成了true 。
·org.quartz.scheduler.wrapJobExecutionInUserTransaction
若是你要 Quartz 在调用你的 Job 的 execute 以前启动一个 UserTransaction 的话,设置这个属性为 true 。这个事物将在 Job 的execute 方法完成和 JobDataMap (假如是一个StatefulJob ) 更新后提交。默认值为 false 。
·org.quartz.scheduler.jobFactory.class
这是所用的 JobFactory 的类名称。默认为 org.quartz.simpl.SimpleJobFactory 。你也能够试试org.quartz.simpl.PropertySettingJobFactory 。一个 Job 工厂负责产生 Job 类的实例。SimpleFactory 类是调用 Job 类的newInstance() 方法。PropertySettingJobFactory 也会调用newInstance() ,但还会使用 JobDataMap 中的内容以反射方式设置 Job Bean 的属性。
小结
Quartz提供了最为丰富的任务调度功能,不但能够制定周期性运行的任务调度方案,还可让你按照日历相关的方式进行任务调度。Quartz框架的重要组件包括Job、JobDetail、Trigger、Scheduler以及辅助性的JobDataMap和SchedulerContext。Quartz拥有一个线程池,经过线程池为任务提供执行线程,你能够经过配置文件对线程池进行参数定制。Quartz的另外一个重要功能是可将任务调度信息持久化到数据库中,以便系统重启时可以恢复已经安排的任务。此外,Quartz还拥有完善的事件体系,容许你注册各类事件的监听器。