http://blog.itpub.net/11627468/viewspace-1763498/
一、quartz核心概念
先来看一张图:
scheduler |
任务调度器 |
trigger |
触发器,用于定义任务调度时间规则 |
job |
任务,即被调度的任务 |
misfire |
错过的,指本来应该被执行但实际没有被执行的任务调度 |
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实例;
quartz.properties文件
Quartz有一个叫做quartz.properties的配置文件,它允许你修改框架运行时环境。缺省是使用Quartz.jar里面的quartz.properties文件。当然,你应该创建一个quartz.properties文件的副本并且把它放入你工程的classes目录中以便类装载器找到它。
一旦将Quartz.jar文件和第三方库加到自己的工程里面并且quartz.properties文件在工程的classes目录中,就可以创建作业了。然而,在做这之前,我们暂且回避一下先简短讨论一下Quartz架构。
Quartz内部架构
在规模方面,Quartz跟大多数开源框架类似。大约有300个Java类和接口,并被组织到12个包中。这可以和Apache Struts把大约325个类和接口以及组织到11个包中相比。尽管规模几乎不会用来作为衡量框架质量的一个特性,但这里的关键是quarts内含很多功能,这些功能和特性集是否成为、或者应该成为评判一个开源或非开源框架质量的因素。
Quartz调度器
Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于多线程的架构。
启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。这就是Quartz怎样能并发运行多个作业的原理。Quartz依赖一套松耦合的线程池管理部件来管理线程环境。本文中,我们会多次提到线程池管理,但Quartz里面的每个对象是可配置的或者是可定制的。所以,例如,如果你想要插进自己线程池管理设施,我猜你一定能!
作业
用Quartz的行话讲,作业是一个执行任务的简单Java类。任务可以是任何Java代码。只需你实现org.quartz.Job接口并且在出现严重错误情况下抛出JobExecutionException异常即可。
Job接口包含唯一的一个方法execute(),作业从这里开始执行。一旦实现了Job接口和execute()方法,当Quartz确定该是作业运行的时候,它将调用你的作业。Execute()方法内就完全是你要做的事情。
作业管理和存储
作业一旦被调度,调度器需要记住并且跟踪作业和它们的执行次数。如果你的作业是30分钟后或每30秒调用,这不是很有用。事实上,作业执行需要非常准确和即时调用在被调度作业上的execute()方法。Quartz通过一个称之为作业存储(JobStore)的概念来做作业存储和管理。
有效作业存储
Quartz提供两种基本作业存储类型。第一种类型叫做RAMJobStore,它利用通常的内存来持久化调度程序信息。这种作业存储类型最容易配置、构造和运行。对许多应用来说,这种作业存储已经足够了。
然而,因为调度程序信息是存储在被分配给JVM的内存里面,所以,当应用程序停止运行时,所有调度信息将被丢失。如果你需要在重新启动之间持久化调度信息,则将需要第二种类型的作业存储。
第二种类型的作业存储实际上提供两种不同的实现,但两种实现一般都称为JDBC作业存储。两种JDBC作业存储都需要JDBC驱动程序和后台数据库来持久化调度程序信息。这两种类型的不同在于你是否想要控制数据库事务或这释放控制给应用服务器例如BEA's WebLogic或Jboss。(这类似于J2EE领域中,Bean管理的事务和和容器管理事务之间的区别)这两种JDBC作业存储是:
· JobStoreTX:当你想要控制事务或工作在非应用服务器环境中是使用
· JobStoreCMT:当你工作在应用服务器环境中和想要容器控制事务时使用。
JDBC作业存储为需要调度程序维护调度信息的用户而设计。
作业和触发器
Quartz设计者做了一个设计选择来从调度分离开作业。Quartz中的触发器用来告诉调度程序作业什么时候触发。框架提供了一把触发器类型,但两个最常用的是SimpleTrigger和CronTrigger。SimpleTrigger为需要简单打火调度而设计。
典型地,如果你需要在给定的时间和重复次数或者两次打火之间等待的秒数打火一个作业,那么SimpleTrigger适合你。另一方面,如果你有许多复杂的作业调度,那么或许需要CronTrigger。
CronTrigger是基于Calendar-like调度的。当你需要在除星期六和星期天外的每天上午10点半执行作业时,那么应该使用CronTrigger。正如它的名字所暗示的那样,CronTrigger是基于Unix克隆表达式的。
作为一个例子,下面的Quartz克隆表达式将在星期一到星期五的每天上午10点15分执行一个作业。
0 15 10 ? * MON-FRI
下面的表达式
0 15 10 ? * 6L 2002-2005
将在2002年到2005年的每个月的最后一个星期五上午10点15分执行作业。你不可能用SimpleTrigger来做这些事情。你可以用两者之中的任何一个,但哪个跟合适则取决于你的调度需要。
trigger还有一个重要的属性misfire;如果scheduler关闭了,或者Quartz线程池中没有可用的线程来执行job,此时持久性的trigger就会错过(miss)其触发时间,即错过触发(misfire)。不同类型的trigger,有不同的misfire机制。它们默认都使用“智能机制(smart policy)”,即根据trigger的类型和配置动态调整行为。当scheduler启动的时候,查询所有错过触发(misfire)的持久性trigger。然后根据它们各自的misfire机制更新trigger的信息。当你在项目中使用Quartz时,你应该对各种类型的trigger的misfire机制都比较熟悉,这些misfire机制在JavaDoc中有说明。关于misfire机制的细节,会在讲到具体的trigger时作介绍。
调度一个作业
让我们通过看一个例子来进入实际讨论。现假定你管理一个部门,无论何时候客户在它的FTP服务器上存储一个文件,都得用电子邮件通知它。我们的作业将用FTP登陆到远程服务器并下载所有找到的文件。
然后,它将发送一封含有找到和下载的文件数量的电子邮件。这个作业很容易就帮助人们整天从手工执行这个任务中解脱出来,甚至连晚上都无须考虑。我们可以设置作业循环不断地每60秒检查一次,而且工作在7×24模式下。这就是Quartz框架完全的用途。
首先创建一个Job类,将执行FTP和Email逻辑。下例展示了Quartz的Job类,它实现了org.quartz.Job接口。
用调度器调用作业
首先创建一个作业,但为使作业能被调度器调用,你得向调度程序说明你的作业的调用时间和频率。这个事情由与作业相关的触发器来完成。因为我们仅仅对大约每60秒循环调用作业感兴趣,所以打算使用SimpleTrigger。
作业和触发器通过Quartz调度器接口而被调度。我们需要从调度器工厂类取得一个调度器的实例。最容易的办法是调用StdSchedulerFactory这个类上的静态方法getDefaultScheduler()。
使用Quartz框架,你需要调用start()方法来启动调度器。例3的代码遵循了大多数Quartz应用的一般模式:创建一个或多个作业,创建和设置触发器,用调度器调度作业和触发器,启动调度器。
编程调度同声明性调度
我们通过编程的方法调度我们的ScanFTPSiteJob作业。就是说,我们用Java代码来设置作业和触发器。Quartz框架也支持在xml文件里面申明性的设置作业调度。申明性方法允许我们更快速地修改哪个作业什么时候被执行。
Quartz框架有一个插件,这个插件负责读取xml配置文件。xml配置文件包含了关于启动Quartz应用的作业和触发器信息。所有xml文件中的作业连同相关的触发器都被加进调度器。你仍然需要编写作业类,但配置那些作业类的调度器则非常动态化。你可以将xml文件中的元素跟例3代码作个比较,它们从概念上来看是相同的。使用申明性方法的好处是维护变得极其简单,只需改变xml配置文件和重新启动Quartz应用即可。无须修改代码,无须重新编译,无须重新部署。
有状态和无状态作业
作业到是无状态的。这意味着在两次作业执行之间,不会去维护作业执行时JobDataMap的状态改变。如果你需要能增、删,改JobDataMap的值,而且能让作业在下次执行时能看到这个状态改变,则需要用Quartz有状态作业。
Quartz有状态作业实现了org.quartz.StatefulJob接口。
无状态和有状态作业的关键不同是有状态作业在每次执行时只有一个实例。大多数情况下,有状态的作业不回带来大的问题。然而,如果你有一个需要频繁执行的作业或者需要很长时间才能完成的作业,那么有状态作业可能给你带来伸缩性问题。
监听器和插件
每个人都喜欢监听和插件。今天,几乎下载任何开源框架,你必定会发现支持这两个概念。监听是你创建的Java类,当关键事件发生时会收到框架的回调。例如,当一个作业被调度、没有调度或触发器终止和不再打火时,这些都可以通过设置来来通知你的监听器。Quartz框架包含了调度器监听、作业和触发器监听。你可以配置作业和触发器监听为全局监听或者是特定于作业和触发器的监听。
一旦你的一个具体监听被调用,你就能使用这个技术来做一些你想要在监听类里面做的事情。例如,你如果想要在每次作业完成时发送一个电子邮件,你可以将这个逻辑写进作业里面,也可以JobListener里面。写进JobListener的方式强制使用松耦合有利于设计上做到更好。
Quartz插件是一个新的功能特性,无须修改Quartz源码便可被创建和添加进Quartz框架。他为想要扩展Quartz框架又没有时间提交改变给Quartz开发团队和等待新版本的开发人员而设计。如果你熟悉Struts插件的话,那么完全可以理解Quartz插件的使用。
与其Quartz提供一个不能满足你需要的有限扩展点,还不如通过使用插件来拥有可修整的扩展点。
集群Quartz应用
Quartz应用能被集群,是水平集群还是垂直集群取决于你自己的需要。集群提供以下好处:
· 伸缩性
· 高可用性
· 负载均衡
目前,Quartz只能借助关系数据库和JDBC作业存储支持集群。将来的版本这个制约将消失并且用RAMJobStore集群将是可能的而且将不需要数据库的支持。
四、结构与流程分析
1、定时器的启动
参考:http://seanhe.iteye.com/blog/691835
参考这张图,首先quartz的加载可以有两种方式:
方式1:通过servlet,参考:http://blog.csdn.net/ychatou1220/article/details/5806914
方式2:通过spring,例如:
点击(此处)折叠或打开
点击(此处)折叠或打开
再总结一下类的结构:
1、StdSchedulerFactory是工厂类,还有一个工厂类DirectSchedulerFactory比较简单,而StdSchedulerFactory是可以加载各种属性的。
属性的加载initialize方法,Contants里面都是参数,可以按说明在quartz.properties上加。
2、StdSchedule只是QuartzSchedule的一个包装类,方法更清晰。
3、QuartzScheduler是整个定时任务框架工作的核心类,上面的类图仅仅展现了QuartzScheduler中几个核心成员。
4、QuartzSchedulerResources可以认为是存放一切配置以及通过配置初始化出来的一些资源的容器,其中包括了存储job定义的jobStor
5、JobStore可以有多种实现,我们使用的是默认的RAMJobStore;
6、ThreadPool,还有一个非常重要的对象就是ThreadPool,这个线程池管理着执行我们定义的Job所需的所有线程。这个线程池的大小配置就是通过我上面提到过的“org.quartz.threadPool.threadCount”进行配置的。QuartzScheduler另一个重要成员就是QuartzSchedulerThread,没有这个线程的话我们所有定义的任务都不会被触发执行,也就是说它是Quartz后台的“守护线程”,它不断的去查找合适的job并触发这些Job执行。
7、QuartzSchedulerThread主要业务逻辑在上面已经讲了。
2、触发点火
点击(此处)折叠或打开
点击(此处)折叠或打开
点击(此处)折叠或打开
点击(此处)折叠或打开
3、在线程中运行任务
参考:http://blog.csdn.net/cutesource/article/details/4965520
getSchedule只是启动了QuartzSchedulerThread线程,开关未打开。
start()才是打开QuartzSchedulerThread的开关,真正开始线程轮循。
当总线程QuartzSchedulerThread处理完了数据库对trigger操作后,就开始把任务放到线程中执行了。
点击(此处)折叠或打开
点击(此处)折叠或打开
workThread线程执行前,当fired_trigger表由ACQUIRED状态修改为EXECUTING状态时,trigger表由ACQUIRED状态变为WAITING状态。
通过测试,如果是批量执行的话,时间精度是没有控制的。
job1每5,15,25秒执行
job2每6,16,26秒执行
job3每7,17,27秒执行
job4每8,18,28秒执行
代码是:
// job 1 will run every 5,15,25 second JobDetail job = newJob(SimpleJob.class).withIdentity("job1", "group1").build(); CronTrigger trigger = newTrigger().withIdentity("trigger1", "group1").withSchedule(cronSchedule("5,15,25 * * * * ?")) .build(); Date ft = sched.scheduleJob(job, trigger); log.info(job.getKey() + " has been scheduled to run at: " + ft + " and repeat based on expression: " + trigger.getCronExpression()); // job 2 will run every 6,16,26 second JobDetail job2 = newJob(SimpleJob.class).withIdentity("job2", "group1").build(); CronTrigger trigger2 = newTrigger().withIdentity("trigger2", "group1").withSchedule(cronSchedule("6,16,26 * * * * ?")) .build(); Date ft2 = sched.scheduleJob(job2, trigger2); log.info(job2.getKey() + " has been scheduled to run at: " + ft2 + " and repeat based on expression: " + trigger2.getCronExpression()); // job 3 will run every 7,17,27 second JobDetail job3 = newJob(SimpleJob.class).withIdentity("job3", "group1").build(); CronTrigger trigger3 = newTrigger().withIdentity("trigger3", "group1").withSchedule(cronSchedule("7,17,27 * * * * ?")) .build(); Date ft3 = sched.scheduleJob(job3, trigger3); log.info(job3.getKey() + " has been scheduled to run at: " + ft3 + " and repeat based on expression: " + trigger3.getCronExpression()); // job 4 will run every 8,18,28 second JobDetail job4 = newJob(SimpleJob.class).withIdentity("job4", "group1").build(); CronTrigger trigger4 = newTrigger().withIdentity("trigger4", "group1").withSchedule(cronSchedule("8,18,28 * * * * ?")) .build(); Date ft4 = sched.scheduleJob(job4, trigger4); log.info(job4.getKey() + " has been scheduled to run at: " + ft4 + " and repeat based on expression: " + trigger4.getCronExpression());
quartz.properties增加批量处理的配置及数据库的配置:
#批量选triggerorg.quartz.scheduler.batchTriggerAcquisitionMaxCount: 5
增加批量配置的测试的结果是5,15,25秒时每个任务1,2,3,4都执行了:
[INFO] 21 八月 10:50:05.118 上午 DefaultQuartzScheduler_Worker-3 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job1 executing at Fri Aug 21 10:50:05 CST 2015[INFO] 21 八月 10:50:05.119 上午 DefaultQuartzScheduler_Worker-6 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job4 executing at Fri Aug 21 10:50:05 CST 2015[INFO] 21 八月 10:50:05.119 上午 DefaultQuartzScheduler_Worker-5 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job3 executing at Fri Aug 21 10:50:05 CST 2015[INFO] 21 八月 10:50:05.118 上午 DefaultQuartzScheduler_Worker-4 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job2 executing at Fri Aug 21 10:50:05 CST 2015[INFO] 21 八月 10:50:15.114 上午 DefaultQuartzScheduler_Worker-7 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job1 executing at Fri Aug 21 10:50:15 CST 2015[INFO] 21 八月 10:50:15.115 上午 DefaultQuartzScheduler_Worker-9 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job3 executing at Fri Aug 21 10:50:15 CST 2015[INFO] 21 八月 10:50:15.116 上午 DefaultQuartzScheduler_Worker-10 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job4 executing at Fri Aug 21 10:50:15 CST 2015[INFO] 21 八月 10:50:15.115 上午 DefaultQuartzScheduler_Worker-8 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job2 executing at Fri Aug 21 10:50:15 CST 2015[INFO] 21 八月 10:50:25.107 上午 DefaultQuartzScheduler_Worker-2 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job1 executing at Fri Aug 21 10:50:25 CST 2015[INFO] 21 八月 10:50:25.107 上午 DefaultQuartzScheduler_Worker-1 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job2 executing at Fri Aug 21 10:50:25 CST 2015[INFO] 21 八月 10:50:25.108 上午 DefaultQuartzScheduler_Worker-3 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job3 executing at Fri Aug 21 10:50:25 CST 2015[INFO] 21 八月 10:50:25.108 上午 DefaultQuartzScheduler_Worker-4 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job4 executing at Fri Aug 21 10:50:25 CST 2015
#批量选triggerorg.quartz.scheduler.batchTriggerAcquisitionMaxCount: 5
结果是分别执行,精确到秒的。
[INFO] 21 八月 10:59:05.040 上午 DefaultQuartzScheduler_Worker-5 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job1 executing at Fri Aug 21 10:59:05 CST 2015[INFO] 21 八月 10:59:06.041 上午 DefaultQuartzScheduler_Worker-6 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job2 executing at Fri Aug 21 10:59:06 CST 2015[INFO] 21 八月 10:59:07.041 上午 DefaultQuartzScheduler_Worker-7 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job3 executing at Fri Aug 21 10:59:07 CST 2015[INFO] 21 八月 10:59:08.028 上午 DefaultQuartzScheduler_Worker-8 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job4 executing at Fri Aug 21 10:59:08 CST 2015[INFO] 21 八月 10:59:15.031 上午 DefaultQuartzScheduler_Worker-9 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job1 executing at Fri Aug 21 10:59:15 CST 2015[INFO] 21 八月 10:59:16.044 上午 DefaultQuartzScheduler_Worker-10 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job2 executing at Fri Aug 21 10:59:16 CST 2015[INFO] 21 八月 10:59:17.042 上午 DefaultQuartzScheduler_Worker-1 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job3 executing at Fri Aug 21 10:59:17 CST 2015[INFO] 21 八月 10:59:18.040 上午 DefaultQuartzScheduler_Worker-2 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job4 executing at Fri Aug 21 10:59:18 CST 2015[INFO] 21 八月 10:59:25.042 上午 DefaultQuartzScheduler_Worker-3 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job1 executing at Fri Aug 21 10:59:25 CST 2015[INFO] 21 八月 10:59:26.041 上午 DefaultQuartzScheduler_Worker-4 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job2 executing at Fri Aug 21 10:59:26 CST 2015[INFO] 21 八月 10:59:27.039 上午 DefaultQuartzScheduler_Worker-5 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job3 executing at Fri Aug 21 10:59:27 CST 2015[INFO] 21 八月 10:59:28.041 上午 DefaultQuartzScheduler_Worker-6 [org.quartz.examples.example16.SimpleJob]SimpleJob says: group1.job4 executing at Fri Aug 21 10:59:28 CST 2015
所以,batchTriggerAcquisitionMaxCount这个参数是能提高性能,不管是数据库方式还是内存集群方式,批量执行加锁解锁当然可以更快。但是精度会有损失。
因此适用于每一秒都有多个任务执行的情况。
比如说每秒有1000个任务同时执行,那么可以设置开批量功能。对于差距1s内可以忽略不计的。
在生产上建设可以把可批量执行的任务放入一个集群。
把对精度、稳定性要求高的任务放入另一个集群。