`

深入解读Quartz及应用

 
阅读更多

一、Quartz基本概念

 

Quartz 是 OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。该项目于 2009 年被 Terracotta 收购,目前是 Terracotta 旗下的一个项目。读者可以到 http://www.quartz-scheduler.org/站点下载 Quartz 的最新发布版本及其源代码。QCT系统使用的是版本 1.6.0,因此本文内容基于该版本。本文不仅介绍如何应用 Quartz 进行开发,也对其内部实现原理作一定讲解。

 

二、Quartz的特点

Ø强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;

 

Ø灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;

 

Ø分布式和集群能力,支持分布式和集群环境中应用Quartz进行任务调度。

 

ØQuartz能够以独立的方式运行(在它自己的Java虚拟机中),可以通过RMI使用Quartz
 
三、核心元素
ØJob:是一个接口,此接口只有一个方法,void exceuteJobExecutionContext context) ,开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;
ØJobDetailQuartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类描述Job的实现类及其相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色;
 
ØTrigger:触发器,描述触发Job执行的时间触发规则。主要有SimpleTrigger  CronTriggerNthIncludeDayTrigger三种触发器,当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择。而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案,如每天中午12执行等;
ØCalendarorg.quartz.Calendarjava.util.Calendar不同,它是一些日历特定时间点的集合。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点;
ØScheduler:代表一个Quartz的独立运行容器,TriggerJobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler可以将Trigger绑定到某一JobDetail中,这样当 Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler主要有三种:RemoteMBeanSchedulerRemoteSchedulerStdSchedulerScheduler拥有一个SchedulerContext,它类似ServeletContext,保存着Scheduler上下文信息,注册到该SchedulerJobTrigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据;
ØListenerQuartz提供了listener功能。主要包括三种listenerJobListenerTriggerListenerSchedulerListener。可对JobTriggerScheduler内部行为进行监听;
ØThreadPoolScheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
 
四、Quartz 任务调度的基本实现原理
Quartz中,Job用于表示被调度的任务。主要有两种类型的Job:无状态(sateless)和有状态的(stateful)。对于同一个Trigger来说,有状态的Job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:volatilitydurability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有Trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。Scheduler内部组件图:
Quartz核心元素关系图


 Quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。线程图如下:


 Scheduler调度线程主要有两个:执行常规的线程和执行misfired trigger的线程。常规调度线程轮询存储的所有Trigger,如果有需要触发的trigger,即到达了下一个触发时间,则从任务执行线程池中获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfired trigger,如果有就根据misfire的策略分别处理。下图描述了这两个线程的基本流程
Quartz调度线程流程图

 
五、 通过源码了解如何实现按时间调度
Quartz最核心的地方是QuartzSchedulerThread运行机制。下面解析下它的run方法:

以上是run的最开头的一段,不难看出这是在等待schedulerstart,实际上Quartz就是通过线程的waitsleep来实现时间调度。继续看代码:

 这段代码是从jobStore里拿到下一个要执行的trigger,一般情况下jobStore使用的是RAMJobStore,即trigger等相关信息存放在内存里,如果需要把任务持久化就得使用可持久化JobStore。继续看代码:

此段代码是计算下一个trigger的执行时间和现在系统时间的差,然后通过循环线程sleep的方式暂停住此线程,一直等到trigger的执行时间点。继续看代码:

此段代码就是包装trigger,然后通过以JobRunShell为载体,在threadpool里执行trigger所关联的jobDetail
 
六、 数据存储
Quartz中的triggerjob需要存储下来才能被使用。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将triggerjob存储在内存中,而JobStoreSupport是基于jdbctriggerjob存储到数据库中。RAMJobStore的存取速度快,但缺点是其在系统被停止后所有的数据都会丢失。而JobStoreSupport使用一个驱动代理StdJDBCDelegate来操作triggerjob数据存储到数据库。StdJDBCDelegate实现了大部分基于标准JDBC的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此不同的数据库需要扩展StdJDBCDelegate以实现这些特殊处理。Quartz已自带了一些数据库的扩展实现,可以直接使用,如下图:

 Quartz数据表

 七、 Quartz与Spring集成
方法一、 Spring org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean类,使用此方法使开发人员对Quartz完全透明,需要实现定时任务的方法只是一个普通方法。

 第一种方式的Java代码:

 方法二、借助于Springorg.springframework.scheduling.quartz.JobDetailBean的类功能,继承 Spring封装Quartzorg.springframework.scheduling.quartz.QuartzJobBean,实现 executeInternal方法,executeInternal方法中调用业务类。

 第二种方式的Java代码:

 八、 企业级开发中常见应用

在应用Quartz进行企业级的开发是,有些问题会经常遇到。下面介绍企业开发中常见的一些问题及通常的解决办法:

  应用一:如何使用不同类型的Trigger

  前面我们提到Quartz中三种类型的TriggerSimpleTriggerCronTriggerNthIncludedDayTrigger

  SimpleTrigger一般用于实现每隔一定时间执行任务,以及重复多少次,如每2小时执行一次,重复执行5次。SimpleTrigger内部实现机制是通过计算间隔时间来计算下次的执行时间,这就导致其不合适调度定时的任务。例如我们想每天的8:00AM执行任务,如果使用SimpleTrigger的话间隔时间就是一天。主要这里就有一个问题,即当有misfired的任务并且恢复执行时,该执行时间是随机的(取决于何时执行misfired的任务,例如某天的9:00PM)。这会导致之后每天的执行时间都会变成9:00PM,而不是我原来期望的8:00AM

 

   CronTrigger类似于Linux上的任务调度命令crontab,即利用一个包含7个字段的表达式来表示时间调度方式。例如,“0 15 10 * * ? *”表示每天的10:15AM执行任务。对于涉及到星期和月份的调度,CronTrigger是最合适的,甚至某些情况下是唯一选择。例如,“0 10 14 3 WED”表示三月份的每个星期三的下午14:10PM执行任务。使用者可以在具体用的该trigger时再详细了解每个字段的含义。

 

   NthIncludedDayTrigger的用途比较简单明确,即用于每间隔一个周期的第几天调度任务,例如,每个月的第2天执行指定的任务。

 

 

应用二:使用有状态(StatefulJob)还是无状态的任务(Job)

   Quartz中,Job是一个接口,企业应用需要实现这个接口定义自己的任务。基本来说,任务分为有状态和无状态两种。实现Job接口的任务缺省为无状态的。Quartz中还有另外一个接口StatefulJob。实现StatefulJob接口的任务为有状态的,下图列出了QuartzJob接口的定义以及一些自带的实现类:

无状态任务一般指可以并发的任务,即任务之间是独立的不会相互干扰。例如我们定义一个trigger,每2分钟执行一次,但是某些情况下一个任务可能需要3分钟才能执行完,这样,在上一个任务还处在执行状态时,下一次触发时间已经到了。对于无状态任务,只要触发时间到了就会被执行,因为几个相同任务可以并发执行。但是对于有状态任务来说,是不能并发执行的,同一时间只能有一个任务在执行。

      如某些任务需要对数据库中的数据进行增删改处理。这些任务不能并发执行,否则会造成数据混乱。因此我们使用StatefulJob接口。现在回到上面的例子,任务每 2 分钟执行一次,若某次任务执行了 5 分钟才完成,Quartz 会怎么处理呢?按照 trigger 的规则,第 2 分钟和第 4 分钟分别会有一次预定的触发执行,但是由于是有状态任务,因此实际不会被触发。在第 5 分钟第一次任务执行完毕时,Quartz 会把第 2 和第 4 分钟的两次触发作为 misfired job 进行处理。对于 misfired jobQuartz 会查看其 misfire 策略是如何设定的,如果是立刻执行,则会马上启动一次执行,如果是等待下次执行,则会忽略错过的任务,而等待下(即第 6 分钟)触发执行。

 

 

应用三:如何设置Quartz的线程池和并发任务

nQuartz中自带了一个线程的实现:SimpleThreadPool。类如其名,这只是线程池的一种简单实现,没有提供动态自发调整等高级特性。
nQuartz提供了一个配置参数: org.quartz.threadPool.threadCount,可以在初始化时设定线程池的线程数量,但是一次设定后不能再修改。假定这个数目是 10,则在并发任务达到 10 个以后,再有触发的任务就无法被执行了,只能等待有空闲线程的时候才能得到执行。因此有些 trigger 就可能被 misfire。但是必须指出一点,这个初始线程数并不是越大越好。当并发线程太多时,系统整体性能反而会下降,因为系统把很多时间花在了线程调度上。根据一般经验,这个值在 10 -- 50 比较合适。
n对于一些注重性能的线程池来说,会根据实际线程使用情况进行动态调整,例如初始线程数,最大线程数,空闲线程数等。在使用者应用中,如果有更好的线程池,则可以在配置文件中通过下面参数替换 SimpleThreadPoolorg.quartz.threadPool.class = myapp.GreatThreadPool

 

应用四:如何处理Misfired任务

   Quartz应用中,misfired job是经常遇到的情况,一般来说,下面这些原因可能造成misfired job

   1)系统因为某些原因被重启。在系统关闭到重启之间的一段时间里,可能有些任务会被misfire

   2Trigger被暂停(pause)的一段时间里,有些任务可能会被misfire

   3)线程池中所有线程都被占用,导致任务无法被触发执行,造成misfire

   4)有状态任务在下次触发时间到达时,上次执行还没有结束;

   为了处理misfired jobQuartz中为trigger定义了处理策略,主要有下面两种:

   MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:针对 misfired job 马上执行一次;

   MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次触发。

  • 大小: 15.9 KB
  • 大小: 5.2 KB
  • 大小: 7.9 KB
  • 大小: 30.6 KB
  • 大小: 23.8 KB
  • 大小: 42.3 KB
  • 大小: 42.1 KB
  • 大小: 19.1 KB
  • 大小: 3.8 KB
  • 大小: 12.1 KB
  • 大小: 61.2 KB
  • 大小: 8.5 KB
  • 大小: 73.1 KB
  • 大小: 13.9 KB
  • 大小: 7.8 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics