quartz使用入门篇【面试+工作】

quartz使用入门篇【面试+工作】

下篇文章将具体介绍使用案例

了解quartz

1.引入

你曾经需要应用执行一个任务吗?这个任务每天或每周星期二晚上11:30,或许仅仅每个月的最后一天执行。一个自动执行而无须干预的任务在执行过程中如果发生一个严重错误,应用能够知到其执行失败并尝试重新执行吗?你和你的团队是用Java编程吗?如果这些问题中任何一个你回答是,那么你应该使用Quartz调度器。

旁注:Matrix目前就大量使用到了Quartz。比如,排名统计功能的实现,在Jmatrix里通过Quartz定义了一个定时调度作业,在每天凌晨一点,作业开始工作,重新统计大家的Karma和排名等。还有,RSS文件的生成,也是通过Quartz定义作业,每隔半个小时生成一次RSS XML文件。

2.为什么研发团队会选择quartz

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开源协议,允许代码修改,再商业发布。

3.quartz集群 关于quartz集群网上文章介绍很多,拿一张常见的图来说明。

  • Quartz集群中的每个节点是一个独立的Quartz应用,它又管理其它的节点。
  • 需要分别对每个节点分别启动或停止。不像应用服务器的集群,独立的Quartz节点并不与另一个节点或是管理节点通信。
  • Quartz应用是通过数据库表来感知到另一应用。
  • Quartz应用定时15秒同步一次心跳到数据库。
  • Quartz通过数据库行锁来解决分布式环境下数据一致性问题。

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源码进行分析。

4.如何使定时任务的开发方便,易于管理。

阿里开源项目easySchedule在quartz集群的基础上搭建了一个简单的管理平台。解决了可视化、易配置、统一监控告警功能。 实现调度与执行的分离,使任务不需要再去关注定时,只需要实现任务接口即可。 调度通过HTTP来调用执行任务。

easySchedule系统特点:

1、 Server和Client分别支持集群和分布式部署

2、 任务的执行与调度分离

3、 可视化管理所有任务

4、 任务状态持久化于DB

5、 完善的日志跟踪和告警策略

6、 任务支持异步调度

7、 灵活支持各种自定义任务,扩展方便


由浅入深

一、quartz核心概念

先来看一张图:

scheduler

任务调度器

trigger

触发器,用于定义任务调度时间规则

job

任务,即被调度的任务

misfire

错过的,指本来应该被执行但实际没有被执行的任务调度

  • Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;
  • JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。
  • Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午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进行定点排除。
  • 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使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

二、如何使用 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(); }

2.web中使用Quartz

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>

3.以下是我在应用的的一个基本配置:

#---------调度器属性---------------- 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

4.属性的介绍

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设计分析

1.quartz.properties文件

Quartz有一个叫做quartz.properties的配置文件,它允许你修改框架运行时环境。缺省是使用Quartz.jar里面的quartz.properties文件。当然,你应该创建一个quartz.properties文件的副本并且把它放入你工程的classes目录中以便类装载器找到它。

一旦将Quartz.jar文件和第三方库加到自己的工程里面并且quartz.properties文件在工程的classes目录中,就可以创建作业了。然而,在做这之前,我们暂且回避一下先简短讨论一下Quartz架构。

2.Quartz内部架构

在规模方面,Quartz跟大多数开源框架类似。大约有300个Java类和接口,并被组织到12个包中。这可以和Apache Struts把大约325个类和接口以及组织到11个包中相比。尽管规模几乎不会用来作为衡量框架质量的一个特性,但这里的关键是quarts内含很多功能,这些功能和特性集是否成为、或者应该成为评判一个开源或非开源框架质量的因素。

3.Quartz调度器

Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于多线程的架构。

  启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。这就是Quartz怎样能并发运行多个作业的原理。Quartz依赖一套松耦合的线程池管理部件来管理线程环境。本文中,我们会多次提到线程池管理,但Quartz里面的每个对象是可配置的或者是可定制的。所以,例如,如果你想要插进自己线程池管理设施,我猜你一定能!

4.作业

用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集群将是可能的而且将不需要数据库的支持。


结构与流程分析

1.定时器的启动

参考这张图,首先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主要业务逻辑在上面已经讲了。

2.触发点火

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

接口的方式更解耦,更易扩展。

3.在线程中运行任务

只是启动了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.

4.trigger的状态变化。

最后总结下trigger的状态:

workThread线程执行前,当fired_trigger表由ACQUIRED状态修改为EXECUTING状态时,trigger表由ACQUIRED状态变为WAITING状态。

由于为任务执行完成后,trigger才回到WAITING状态,重新被获取。 所以如果每隔10秒钟执行任务,一个任务要执行8秒钟,则同一时间只有一个线程执行。 如果每隔5秒钟执行任务,一个任务要执行8秒钟,则需要2个线程。 如果每隔M秒钟执行任务,一个任务要执行N秒钟,则需要N/M个线程。


quartz关键点分析

batchTriggerAcquisitionMaxCount的使用

通过测试,如果是批量执行的话,时间精度是没有控制的。

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内可以忽略不计的。

在生产上建设可以把可批量执行的任务放入一个集群。

把对精度、稳定性要求高的任务放入另一个集群。

原文发布于微信公众号 - Java帮帮(javahelp)

原文发表时间:2018-06-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FreeBuf

微软对外披露两个0day漏洞详情

微软近日对外披露了两个0day漏洞详情,其中一个漏洞存在Adobe阅读器中,可被利用导致任意代码执行;另一个漏洞则允许任意代码在Windows kernel内存...

8310
来自专栏owent

libatbus的几个藏得很深的bug

在写这篇文章前,我突然想到以前流行了一段时间的服务器面试题:当一个BUG只有几百万分之一的概率会出现,怎么办?这个问题在这个BUG里只是毛毛雨而已,因为这次的B...

9830
来自专栏安恒网络空间安全讲武堂

护网杯easy laravel ——Web菜鸡的详细复盘学习

复现让我发现了很多读wp以为懂了动手做的时候却想不通的漏掉的知识点(还是太菜orz),也让我对这道题解题逻辑更加理解。所以不要怂,就是干23333!

31730
来自专栏逸鹏说道

2.并发编程~先导篇(下)

代码实例:https://github.com/lotapp/BaseCode/tree/master/python/5.concurrent/Linux/进程...

14540
来自专栏纯洁的微笑

分布式爬虫系统设计、实现与实战:爬取京东、苏宁易购全网手机商品数据+MySQL、HBase存储

35030
来自专栏实战docker

Docker下的Kafka学习之三:集群环境下的java开发

在上一章《Docker下的Kafka学习之二:搭建集群环境》中我们学会了搭建kafka集群环境,今天我们来实战集群环境下的用java发送和消费kafka的消息;...

28650
来自专栏高性能服务器开发

(三)一个服务器程序的架构介绍

本文将介绍我曾经做过的一个项目的服务器架构和服务器编程的一些重要细节。 一、程序运行环境 操作系统:centos 7.0 编译器:gcc/g++ 4.8.3 c...

41550
来自专栏芋道源码1024

Java中高级面试题(4)

这里选了几道高频面试题以及一些解答。不一定全部正确,有一些是没有固定答案的,如果发现有错误的欢迎纠正,如果有更好的回答,热烈欢迎留言探讨。

20800
来自专栏大内老A

WCF后续之旅(17):通过tcpTracer进行消息的路由

对于希望对WCF的消息交换有一个深层次了解的读者来说,tcpTracer绝对是一个不可多得好工具。我们将tcpTracer置于服务和服务代理之间,tcpTrac...

22180
来自专栏高性能服务器开发

(三)一个服务器程序的架构介绍

本文将介绍我曾经做过的一个项目的服务器架构和服务器编程的一些重要细节。 一、程序运行环境 操作系统:centos 7.0 编译器:gcc/g++ 4.8.3 c...

38370

扫码关注云+社区

领取腾讯云代金券