quartz使用入门篇【面试+工作】
你曾经需要应用执行一个任务吗?这个任务每天或每周星期二晚上11:30,或许仅仅每个月的最后一天执行。一个自动执行而无须干预的任务在执行过程中如果发生一个严重错误,应用能够知到其执行失败并尝试重新执行吗?你和你的团队是用Java编程吗?如果这些问题中任何一个你回答是,那么你应该使用Quartz调度器。
旁注:Matrix目前就大量使用到了Quartz。比如,排名统计功能的实现,在Jmatrix里通过Quartz定义了一个定时调度作业,在每天凌晨一点,作业开始工作,重新统计大家的Karma和排名等。还有,RSS文件的生成,也是通过Quartz定义作业,每隔半个小时生成一次RSS XML文件。
java编写的开源作业调度框架设计,用于J2SE和J2EE应用方便集成。
资历够老,创立于1998年,比struts1还早,而且一直在更新(24 Sept 2013: Quartz 2.2.1 Released),文档齐全。
设计清晰简单:核心概念scheduler,trigger,job,jobDetail,listener,calendar
支持集群:org.quartz.jobStore.isClustered 最重要的一点原因是quartz是支持集群的。不然JDK自带Timer就可以实现相同的功能。
支持任务恢复:requestsRecovery
普及面很广,JAVA开发人员比较熟悉。
Apache2.0开源协议,允许代码修改,再商业发布。
Quartz官网: http://quartz-scheduler.org/documentation/quartz-2.x/tutorials/tutorial-lesson-11
问题:由于Quartz的集群基于对表进行行锁,Quartz内部的DB操作大量Trigger存在严重竞争问题,瞬间大量trigger执行。
答:目前只能通过(org.quartz.jobStore.tablePrefix = QRTZ)分表操作,存在长时间lock_wait(新版本据说有提高)。Quartz2.x已经支持可选节点执行job,需要测试Spring最新版本是否支持Quartz的集成。
关于锁的机制,后续文章会对quartz源码进行分析。
阿里开源项目easySchedule在quartz集群的基础上搭建了一个简单的管理平台。解决了可视化、易配置、统一监控告警功能。 实现调度与执行的分离,使任务不需要再去关注定时,只需要实现任务接口即可。 调度通过HTTP来调用执行任务。
easySchedule系统特点:
1、 Server和Client分别支持集群和分布式部署
2、 任务的执行与调度分离
3、 可视化管理所有任务
4、 任务状态持久化于DB
5、 完善的日志跟踪和告警策略
6、 任务支持异步调度
7、 灵活支持各种自定义任务,扩展方便
先来看一张图:
scheduler | 任务调度器 |
---|---|
trigger | 触发器,用于定义任务调度时间规则 |
job | 任务,即被调度的任务 |
misfire | 错过的,指本来应该被执行但实际没有被执行的任务调度 |
二、如何使用 1.学习quartz首先了解三个概念:
调度器:负责调度作业和触发器;
触发器:设置作业执行的时间、参数、条件等;(简单触发器和Cron触发器)
作业:定时任务内容,被执行的程序;
下载必要的jar包,直接去官网下载http://www.opensymphony.com/quartz/download.action,将quartz-x.x.x.jar 和core 和/或 optional 文件夹中的 jar 文件放在项目的文件夹或项目的类路径中
Quartz的几个核心的接口和类为:
Job接口:自己写的“定时程序”实现此接口的void execute(JobExecutionContext arg0)方法,Job还有一类为有状态的StatefulJob接口,如果我们需要在上一个作业执行完后,根据其执行结果再进行下次作业的执行,则需要实现此接口。
Trigger抽象类:调度类(Scheduler)在时间到时调用此类,再由trigger类调用指定的定时程序。
Quertz中提供了两类触发器为:SimpleTrigger,CronTrigger。前者用于实现比较简单的定时功能,例如几点开始,几点结束,隔多长时间执行,共执行多少次等,后者提供了使用表达式来描述定时功能,因此适用于比较复杂的定时描述,例如每个月的最后一个周五,每周的周四等。
JobDetail类:具体某个定时程序的详细描述,包括Name,Group,JobDataMap等。
JobExecutionContext类:定时程序执行的run-time的上下文环境,用于得到当前执行的Job的名字,配置的参数等。
JobDataMap类:用于描述一个作业的参数,参数可以为任何基本类型例如String,float等,也可为某个对象的引用.
JobListener,TriggerListener接口:用于监听触发器状态和作业扫行状态,在特写状态执行相应操作。
JobStore类:在哪里执行定进程序,可选的有在内存中,在数据库中。
开始:边看例题边学习,首先从简单触发器开始……
1).作业通过实现 org.quartz.job 接口,可以使 Java 类变成可执行的。这个类用一条非常简单的输出语句覆盖了 execute(JobExecutionContext context) 方法。
import java.util.Date; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class SimpleQuartzJob implements Job { public SimpleQuartzJob() { } public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("In SimpleQuartzJob - executing its JOB at"+ new Date() + " by " + context.getTrigger().getName()); } }
2).触发器和调度器
public void task() throws SchedulerException{ //通过SchedulerFactory来获取一个调度器 SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); //引进作业程序 JobDetail jobDetail = new JobDetail("jobDetail-s1", "jobDetailGroup-s1", SimpleQuartzJob.class);
//new一个触发器 SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger", "triggerGroup-s1");
//设置作业启动时间
long ctime = System.currentTimeMillis(); simpleTrigger.setStartTime(new Date(ctime));
//设置作业执行间隔 simpleTrigger.setRepeatInterval(10000);
//设置作业执行次数 simpleTrigger.setRepeatCount(10); //设置作业执行优先级默认为5 //simpleTrigger.setPriority(10);
//作业和触发器设置到调度器中 scheduler.scheduleJob(jobDetail, simpleTrigger); //启动调度器 scheduler.start(); }
1).首先在web.xml文件中加入 如下内容(根据自己情况设定)
在web.xml中添加QuartzInitializerServlet,Quartz为能够在web应用中使用,提供了一个QuartzInitializerServlet和一个QuartzInitializerListener,用于在加载web应用时,对quartz进行初始化。
<servlet> <servlet-name> QuartzInitializer </servlet-name> <servlet-class> org.quartz.ee.servlet.QuartzInitializerServlet </servlet-class> <init-param> <param-name>config-file</param-name> <param-value>/quartz.properties</param-value> </init-param> <init-param> <param-name>shutdown-on-unload</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
2).quartz.properties文件的配置(各项属性说明下次写),内容如下:
上面提到了quartz.properties,这是自行指定的,Quartz提供了一个默认的配置文件,可以满足基本的j2se应用,如果在web应用中,我们想把job,trigger配置都写到文件中,就需要自己来写,并指定在初始化时加载我们自己的quratz.properties,位置放在classes下。
org.quartz.scheduler.instanceName = TestScheduler org.quartz.scheduler.instanceId = one
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount = 2 org.quartz.threadPool.threadPriority = 4
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin org.quartz.plugin.jobInitializer.fileName = quartz_job.xml org.quartz.plugin.jobInitializer.overWriteExistingJobs = false org.quartz.plugin.jobInitializer.failOnFileNotFound = true org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin org.quartz.plugin.shutdownhook.cleanShutdown = true
3).quartz_job.xml文件配置(各项属性说明下次写),内容如下:
quartz要使用插件来加载自己的xml配置文件,上面指定加载classes/quartz_job.xml文件。
以Quartz定时任务学习(一)中的简单作业SimpleQuartzJob为例子:
<?xml version="1.0" encoding="UTF-8"?> <quartz> <job> <job-detail> <name>listener1</name> <group>group1</group> <job-class>SimpleQuartzJob</job-class>
</job-detail> <trigger> <cron> <name>job1</name> <group>group1</group> <job-name>listener1</job-name> <job-group>group1</job-group> <cron-expression>0/10 * * * * ?</cron-expression> </cron> </trigger> </job>
</quartz>
#---------调度器属性---------------- org.quartz.scheduler.instanceName = TestScheduler org.quartz.scheduler.instanceId = one
#---------线程配置--------------- org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount = 2 org.quartz.threadPool.threadPriority = 4
#---------作业存储设置------------ org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#---------插件配置-------------
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin org.quartz.plugin.jobInitializer.fileName = quartz_job.xml org.quartz.plugin.jobInitializer.overWriteExistingJobs = false org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin org.quartz.plugin.jobInitializer.fileName = quartz_job.xml org.quartz.plugin.jobInitializer.overWriteExistingJobs = false org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin org.quartz.plugin.shutdownhook.cleanShutdown = true
1).调度器属性:
分别设置调度器的实例名(instanceName) 和实例 ID (instanceId)。属性 org.quartz.scheduler.instanceName 可以是你喜欢的任何字符串。默认名字一般都采用QuartzScheduler,第二个属性org.quartz.scheduler.instanceId和instaneName 属性一样,instanceId 属性也允许任何字符串。这个值必须是在所有调度器实例中是唯一的,尤其是在一个集群当中。假如你想 Quartz 帮你生成这个值的话,可以设置为 AUTO。
2).线程池属性:
这些线程在 Quartz 中是运行在后台担当重任的。threadCount 属性控制了多少个工作者线程被创建用来处理 Job。原则上是,要处理的 Job 越多,那么需要的工作者线程也就越多。threadCount 的数值至少为 1。Quartz 没有限定你设置工作者线程的最大值,但是在多数机器上设置该值超过100的话就会显得相当不实用了,特别是在你的 Job 执行时间较长的情况下。这项没有默认值,所以你必须为这个属性设定一个值。
threadPriority 属性设置工作者线程的优先级。优先级别高的线程比级别低的线程更优先得到执行。threadPriority 属性的最大值是常量 java.lang.Thread.MAX_PRIORITY,等于10。最小值为常量 java.lang.Thread.MIN_PRIORITY,为1。这个属性的正常值是 Thread.NORM_PRIORITY,为5。大多情况下,把它设置为5,这也是没指定该属性的默认值。
最后一个要设置的线程池属性是 org.quartz.threadPool.class。这个值是一个实现了 org.quartz.spi.ThreadPool 接口的类的全限名称。Quartz 自带的线程池实现类是 org.quartz.smpl.SimpleThreadPool,它能够满足大多数用户的需求。这个线程池实现具备简单的行为,并经很好的测试过。它在调度器的生命周期中提供固定大小的线程池。你能根据需求创建自己的线程池实现,如果你想要一个随需可伸缩的线程池时也许需要这么做。这个属性没有默认值,你必须为其指定值。
3).作业存储属性:
作业存储部分的设置描述了在调度器实例的生命周期中,Job 和 Trigger 信息是如何被存储的。把调度器信息存储在内存中非常的快也易于配置。当调度器进程一旦被终止,所有的 Job 和 Trigger 的状态就丢失了。要使 Job 存储在内存中需通过设置 org.quartz.jobStrore.class 属性为 org.quartz.simpl.RAMJobStore,在Cron Trigger 和“作业存储和持久化”会用到的不同类型的作业存储实现。
4).其他插件属性:
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin默认时,JobInitializationPlugin插件会在 classpath 中搜索名为 quartz_jobs.xml 的文件并从中加载 Job 和 Trigger 信息。
quartz_jobs.xml 配置和详解
实例:简单触发器
<?xml version='1.0' encoding='utf-8'?>
<quartz>
<job>
<job-detail>
<name>ScanDirectory</name>
<group>DEFAULT</group>
<description>
A job that scans a directory for files
</description>
<job-class>
org.cavaness.quartzbook.chapter3.ScanDirectoryJob
</job-class>
<volatility>false</volatility>
<durability>false</durability>
<recover>false</recover>
<job-data-map allows-transient-data="true">
<entry>
<key>SCAN_DIR</key>
<value>c:/quartz-book/input</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<simple>
<name>scanTrigger</name>
<group>DEFAULT</group>
<job-name>ScanDirectory</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-06-10 6:10:00 PM</start-time>
<!-- repeat indefinitely every 10 seconds -->
<repeat-count>-1</repeat-count>
<repeat-interval>10000</repeat-interval>
</simple>
</trigger>
</job>
</quartz>
我们添加了属性 org.quartz.plugin.jobInitializer.fileName 并设置该属性值为我们想要的文件名。这个文件名要对 classloader 可见,也就是说要在 classpath 下。
第三方包
通过测试。一个简单任务只需要以下几个包:commons-beanutils.jar、commons-collections.jar、commons-logging.jar、commons-digester.jar、quartz.jar即可
名称 | 必须/备注 | 网址 |
---|---|---|
activation.jar | 主要是 JavaMail 要用到 | http://java.sun.com/products/javabeans/glasgow/jaf.html |
commons-beanutils.jar | 是 | http://jakarta.apache.org/commons/beanutils |
commons-collections.jar | 是 | http://jakarta.apache.org/commons/collections |
commons-dbcp-1.1.jar | 是,假如用到数据库作为作业存储 | http://jakarta.apache.org/commons/dbcp |
commons-digester.jar | 是 | 假如你使用了某些插件,就需要它 |
commons-logging.jar | 是 | http://jakarta.apache.org/commons/logging/ |
commons-pool-1.1.jar | http://jakarta.apache.org/commons/pool/ | |
javamail.jar | 发送 e-mail 用 | http://java.sun.com/products/javamail/ |
jdbc2_0-stdext.jar | 是,假如用到数据库作为作业存储 | http://java.sun.com/products/jdbc/ |
jta.jar | 是,假如用到数据库作为作业存储 | http://java.sun.com/products/jta/database |
quartz.jar | 是 | Quart 框架核心包 |
servlet.jar | 假如使用了Servlet 容器,但容器中应该存在 | http://java.sun.com/products/servlet/ |
log4j.jar | 是,日志 | http://logging.apache.org/ |
Quartz有一个叫做quartz.properties的配置文件,它允许你修改框架运行时环境。缺省是使用Quartz.jar里面的quartz.properties文件。当然,你应该创建一个quartz.properties文件的副本并且把它放入你工程的classes目录中以便类装载器找到它。
一旦将Quartz.jar文件和第三方库加到自己的工程里面并且quartz.properties文件在工程的classes目录中,就可以创建作业了。然而,在做这之前,我们暂且回避一下先简短讨论一下Quartz架构。
在规模方面,Quartz跟大多数开源框架类似。大约有300个Java类和接口,并被组织到12个包中。这可以和Apache Struts把大约325个类和接口以及组织到11个包中相比。尽管规模几乎不会用来作为衡量框架质量的一个特性,但这里的关键是quarts内含很多功能,这些功能和特性集是否成为、或者应该成为评判一个开源或非开源框架质量的因素。
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来做这些事情。你可以用两者之中的任何一个,但哪个跟合适则取决于你的调度需要。
错过触发(misfire)
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集群将是可能的而且将不需要数据库的支持。
参考这张图,首先quartz的加载可以有两种方式: 方式1:通过servlet,参考:上面quartz的web应用 方式2:通过spring,例如:
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="dataSource">
<ref bean="scheduleDataSource" />
</property>
<property name="autoStartup" value="true" />
<property name="schedulerFactoryClass" value="org.quartz.impl.StdSchedulerFactory"></property>
<property name="configLocation" value="classpath:quartz.properties" />
<property name="globalJobListeners">
<list>
<ref bean="jobListener" />
</list>
</property>
</bean>
SpringContext在加载SchedulerFactoryBean时会去加载他的afterPropertiesSet初始化方法。 而SchedulerFactoryBean会去与quartz的StdSchedulerFactory交互初使化配置,StdSchedulerFactory会启动总控制线程QuartzSchedulerThread不停的轮循。
而轮循的代码是:
public void run() {
boolean lastAcquireFailed = false;
while (!halted.get()) {
......
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
if(availThreadCount > 0) {
......
//调度器在trigger队列中寻找30秒内一定数目的trigger(需要保证集群节点的系统时间一致)
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
......
//触发trigger
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
......
//释放trigger
for (int i = 0; i < triggers.size(); i++) {
qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
}
}
}
画成图是这样的:
只看左边的图:普通线程 1).线程是否halt住了,没有的话继续; 2).等待,直到线程池里有线程可处理了; 3).调度器在trigger队列中寻找30s内一定数目的trigger批量执行,1.8.6版本是1, 2.2.1版本默认是1,但可以调整这个值。 同时查到的trigger会通过insertFiredTrigger保存到FIRED_TRIGGER表中。 4).等到时间到。 5).加锁,批量执行>1时才加锁,让集群其它服务结点无法操作。然后取到需要触发的trigger,然后再解锁。 6).点火,在线程池执行一个线程,取触发器、JOB,然后在这个线程执行。 7)).修改数据库状态为正在执行。
再总结一下类的结构:
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主要业务逻辑在上面已经讲了。
if(goAhead) {
try {
//触发trigger
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
if(res != null)
bndles = res;
} catch (SchedulerException se) {
首先要分析一下qsRsrcs.getJobStore(),是JobStoreSupport还是RAMJobStore。JobStoreSupport是数据库方式存Job,RAMJobStore是通过内存的方式存Job。数据库比内存访问要慢,但是数据不会因为服务重启而丢失。JobStoreSupport的子类分为JobStoreTX和JobStoreCMT。JobStoreTX事务自己管理、JobStoreCMT事务交由容器管理。
在quartz.properties里配置,即可以在Factory里被注入。 quartz.properteis:
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
StdSchedulerFactory的instantiate方法中:
// Get JobStore Properties
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
String jsClass = cfg.getStringProperty(PROP_JOB_STORE_CLASS,
RAMJobStore.class.getName());
if (jsClass == null) {
initException = new SchedulerException(
"JobStore class not specified. ");
throw initException;
}
try {
js = (JobStore) loadHelper.loadClass(jsClass).newInstance();
} catch (Exception e) {
initException = new SchedulerException("JobStore class '" + jsClass
+ "' could not be instantiated.", e);
throw initException;
}
SchedulerDetailsSetter.setDetails(js, schedName, schedInstId);
triggersFired的逻辑: 1).triggersFired后,会循环处理每个trigger。 2).selectTriggerState先查出状态,只有可运行状态才是可执行的。 3).selectJobDetail查出JOB的详情。 4).修改trigger的信息为正在执行。1.8.6版本是先删后增。 5).修改FIRED_TRIGGER表中信息为正在执行。 6).修改trigger信息,修改下一次触发时间。
查看QuartzScheduleThread的线程执行时,可以看到执行的结果是保存在TriggerFiredBundle类里的。
....
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
......
for(int i = 0; i < bndles.size(); i++){
TriggerFiredResult result = bndles.get(i);
TriggerFiredBundle bndle = result.getTriggerFiredBundle();
.......
JobRunShell shell = null;
try {
shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
shell.initialize(qs);
} 关于trigger的分类OperableTrigger接口的实现有SimpleTriggerImpl、CronTriggerImpl、CalendarIntervalTriggerImpl等。 注:1.8.6版本中CronTrigger,SimpleTrigger类继承Trigger类
2.2.1版本把CronTrigger,SimpleTrigger都作为接口,实现类为CronTriggerImpl和SimpleTriggerImpl
接口的方式更解耦,更易扩展。
只是启动了QuartzSchedulerThread线程,开关未打开。 start()才是打开QuartzSchedulerThread的开关,真正开始线程轮循。 当总线程QuartzSchedulerThread处理完了数据库对trigger操作后,就开始把任务放到线程中执行了。
for (int i = 0; i < bndles.size(); i++) {
TriggerFiredResult result = bndles.get(i);
TriggerFiredBundle bndle = result.getTriggerFiredBundle();
Exception exception = result.getException();
........................
JobRunShell shell = null;
try {
shell =qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
shell.initialize(qs);
} catch (SchedulerException se) {
qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
continue;
}
if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
// this case should never happen, as it is indicative of the
// scheduler being shutdown or a bug in the thread pool or
// a thread pool being used concurrently - which the docs
// say not to do...
getLog().error("ThreadPool.runInThread() return false!");
qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
}
}
shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle); //这是获取一个执行的shell,并实现runnable接口。 shell.initialize(qs); //初使化shell并建new Job实例化Job接口。
JobDetail jobDetail = bundle.getJobDetail();
Class<? extends Job> jobClass = jobDetail.getJobClass();
。。。。。。
return jobClass.newInstance();
qsRsrcs.getThreadPool().runInThread(shell) //这是把shell真正放到线程池中的一个线程上去运行。 总结一下类图结构:
1).JobRunShell是工作线程执行的核心,它实现了Runnable接口。放入到quartz启动时就创建的线程池中的线程上运行。 2).JobExecutionContextImpl是包含了执行环境需要的变量。由JobRunShell在初使化时创建。 3).线程池SimpleThreadPool在quartz启动时初使化、线程池中的工作线程WorkThread在第一调用initialize方法时创建。 线程池维护3个链表workers、availWorkers、busyWorkers。创建后的线程放入到availWorkers、执行时放入到busyWorkers、执行完后又放回到availWorkers.
最后总结下trigger的状态:
workThread线程执行前,当fired_trigger表由ACQUIRED状态修改为EXECUTING状态时,trigger表由ACQUIRED状态变为WAITING状态。
由于为任务执行完成后,trigger才回到WAITING状态,重新被获取。 所以如果每隔10秒钟执行任务,一个任务要执行8秒钟,则同一时间只有一个线程执行。 如果每隔5秒钟执行任务,一个任务要执行8秒钟,则需要2个线程。 如果每隔M秒钟执行任务,一个任务要执行N秒钟,则需要N/M个线程。
通过测试,如果是批量执行的话,时间精度是没有控制的。
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增加批量处理的配置及数据库的配置:
#批量选trigger
org.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
#批量选trigger
org.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内可以忽略不计的。
在生产上建设可以把可批量执行的任务放入一个集群。
把对精度、稳定性要求高的任务放入另一个集群。