六、Quartz任务持久化和配置管理
(一)、JobStore接口
Quartz中的Scheduler调度器、Job任务、Trigger触发器在前面都已经介绍了,但是未曾提过这些数据是存放在哪里的。要知道,就算不进行持久化,这些信息也应该有个地方进行存储的。Quartz提供了两种不同类型的存储方式,内存存储和数据库存储。这两种方式都是基于org.quartz.spi.JobStore接口来实现的。
我们先看一下下面这个图,这是从Eclipse上截取的:
clipboard.png- org.quartz.spi.JobStore 是任务存储的顶层接口类
- org.quartz.simpl.RAMJobStore 是内存存储机制实现类
- org.quartz.impl.jdbcjobstore.JobStoreSupport 是基于JDBC数据库存储的抽象类
- org.quartz.impl.jdbcjobstore.JobStoreCMT 是受应用容器管理事物的数据库存储实现类
- org.quartz.impl.jdbcjobstore.JobStoreTX 是不受应用容器事物管理的数据库存储实现类
org.quartz.spi.JobStore作为任务存储的顶层接口类,他定义了很多的接口方法,总共可归纳为四类,调度器类、任务类、触发器类和之前未提到的Calendar日期这一类,Calendar主要是配合触发器一起设置一些特殊的触发时间而使用的。在项目开发中,我们无需调用JobStore实现类中的方法,但是了解还是很有必要的,因为可以让我们在项目应用中选择更加适合的存储类型。如何框架提供的存储机制不能满足要求,还可以自定义其他的存储方式,比如文件系统存储,如果真这么干,那就需求自己实现JobStore接口,并且实现大约40个接口方法,可以参考RAMJobStore类来看看框架内部具体做了什么再去实现自己的存储类。
(二)、JobStore接口的几种实现类
接下来我们了解一下上图中提到的几种存储方式。
(1)、使用RAMJobStore内存存储数据
Quartz默认的存储机制就是使用内存进行存储的,我们先看一下Quartz的jar包中的默认配置文件quartz.properties,
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
主要看org.quartz.jobStore.class这个属性,属性值org.quartz.simpl.RAMJobStore就是内存存储机制的实现类。如果需要使用别的存储机制,那就将此值替换为别的实现类即可。
使用内存存储的优点是任务的存储和读取的速度极快,和数据库持久化相比差别还是非常大的,而且框架搭建简单,开箱即用。它的缺点是当Quartz程序或应用程序停止了,伴随在内存中的数据也会被回收,任务等数据就永久丢失了。
使用内存存储时,注意配置文件中只需要保留基本的线程池配置和jobStore的实现类等几个简单的属性就行。如果使用了实现类中没有的属性,启动的时候会报错,当然错误提示也很明显。这里主要想提醒一下之前已经使用了持久化配置,现在想体验一下内存存储的朋友们。
(2)、使用数据库存储数据
通过上面的类图可以看出,JobStoreTX和JobStoreCMT都是JobStoreSupport抽象类的实现类,JobStoreSupport是基于JDBC实现了一些基本的功能的抽象类。如果想要自己实现一套关于JDBC存储方式,那么可以继承此抽象类。
我们先看一下Quartz支持哪些数据库:
·Oracle
·MySQL
·Microsoft SQL Server 2000
·HSQLDB
·PostgreSQL
·DB2
·Cloudscape/Derby
·Pointbase
·Informix
·Firebird
。。。等等,总之兼容JDBC驱动的关系型数据库都可以。
了解了哪些数据库可以使用,接下来就是创建数据库了,Quartz提供了各种数据库的脚本,脚本中有创建表和索引的sql,但是没有创建数据库的sql,需要自己先创建数据库,然后执行创建表和索引的脚本。脚本的创建已经在@一、Quartz集成-下载和安装章节中的第三节讲过,这里不再重复讲述。
下面介绍一下各个表的含义:
表名 | 含义 |
---|---|
QRTZ_CALENDARS | 以 Blob 类型存储 Quartz 的 Calendar 信息 |
QRTZ_CRON_TRIGGERS | 存储CronTrigger触发器信息,包括Cron表达式和时区等信息 |
QRTZ_FIRED_TRIGGERS | 存储已触发的Trigger状态信息和关联的Job执行信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组信息 |
QRTZ_SCHEDULER_STATE | 存储有关Scheduler的状态信息 |
QRTZ_LOCKS | 存储程序锁信息 |
QRTZ_JOB_DETAILS | 存储Job的详细信息 |
QRTZ_JOB_LISTENERS | 存储Job配置的JobListener信息 |
QRTZ_SIMPLE_TRIGGERS | 存储SimpleTrigger触发器信息,包括重复次数,间隔等信息 |
QRTZ_BLOG_TRIGGERS | 存储Blob类型的Trigger,一般用于自定义触发器 |
QRTZ_TRIGGER_LISTENERS | 存储已配置的TriggerListener信息 |
QRTZ_TRIGGERS | 存储已配置的Trigger的信息 |
表的前缀默认都是QRTZ_ 开始,我们先了解一下这个表前缀有什么用。假设项目中需要有两套调度器实例,我们想分别持久化这两个实例信息,此时就需要两套上面的表。为了区分表名称,就在前面加上表前缀。比如:
org.quartz.jobStore.tablePrefix=QRTZ1_
org.quartz.jobStore.tablePrefix=QRTZ2_
QRTZ1_ 和QRTZ2_分别是两套表的前缀,分别配置在不同的quartz.properties中,然后根据两个配置文件分别初始化调度实例。
数据库创建好了,下面先使用JobStoreTX存储机制,我们接着往下看:
1、JobStoreTX
TX就是事物的意思,此存储机制用于Quartz独立于应用容器的事物管理,如果是Tomcat容器管理的数据源,那我们定义的事物也不会传播给Quartz框架内部。通俗的讲就是不管我们的Service服务本身业务代码是否执行成功,只要代码中调用了Quartz API的数据库操作,那任务状态就永久持久化了,就算业务代码抛出运行时异常任务状态也不会回滚到之前的状态。
下面介绍一下使用JobStoreTX配置步骤,所有的配置都是在quartz.properties中完成:
- 第一步配置org.quartz.jobStore.class属性:
org.quartz.jobStore.class= org.quartz.impl.jdbcjobstore.JobStoreTX
- 第二步配置驱动代理,以Mysql为例:
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
下面列出一个可用的数据库代理类表格,方便大家使用,如果表格中没有列出你想要的代理类,那就使用标准的 JDBC 代理:org.quartz.impl.jdbcjobstore.StdDriverDelegate
数据库平台 | Quartz 代理类 |
---|---|
Cloudscape/Derby | org.quartz.impl.jdbcjobstore.CloudscapeDelegate |
DB2 (version 6.x) | org.quartz.impl.jdbcjobstore.DB2v6Delegate |
DB2 (version 7.x) | org.quartz.impl.jdbcjobstore.DB2v7Delegate |
DB2 (version 8.x) | org.quartz.impl.jdbcjobstore.DB2v8Delegate |
HSQLDB | org.quartz.impl.jdbcjobstore.PostgreSQLDelegate |
Oracle | org.quartz.impl.jdbcjobstore.oracle.OracleDelegate |
MS SQL Server | org.quartz.impl.jdbcjobstore.MSSQLDelegate |
Pointbase | org.quartz.impl.jdbcjobstore.PointbaseDelegate |
PostgreSQL | org.quartz.impl.jdbcjobstore.PostgreSQLDelegate |
(WebLogic JDBC Driver) | org.quartz.impl.jdbcjobstore.WebLogicDelegate |
(WebLogic 8.1 with Oracle) | org.quartz.impl.jdbcjobstore.oracle.weblogic.WebLogicOracleDelegate |
- 第三步配置数据源:
org.quartz.jobStore.dataSource=qzDS
org.quartz.dataSource.qzDS.driver= com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL= jdbc:mysql://localhost:3306/testDB
org.quartz.dataSource.qzDS.user= root
org.quartz.dataSource.qzDS.password= admin
org.quartz.dataSource.qzDS.maxConnection= 20
这里要注意两点:
第一是org.quartz.dataSource.qzDS.URL属性名末尾的URL字符串必须是大写,如果写成org.quartz.dataSource.qzDS.url ,那初始化调度实例时就会报错。
第二是注意org.quartz.jobStore.dataSource属性,这个属性的意思是给数据源起一个名字。这里属性值配置的是“qzDS”,你也可以配置成别的任意字符串,比如:“abc”,如果真这么做,那就需要将org.quartz.dataSource.qzDS.driver和其他配置的“qzDS”更换为“abc”,配置:
org.quartz.jobStore.dataSource=abc
org.quartz.dataSource.abc.driver= com.mysql.jdbc.Driver
org.quartz.dataSource.abc.URL= jdbc:mysql://localhost:3306/testDB
org.quartz.dataSource.abc.user= root
org.quartz.dataSource.abc.password= admin
org.quartz.dataSource.abc.maxConnection= 20
那么Quartz为什么设计要org.quartz.jobStore.dataSource属性呢?
这个属性主要的目的就是在同一个数据库中需要使用多套Quartz,一般大家只需要一套数据源就可以完成业务工作,除非有一些特别的需求。比如SaaS模式下可以对不同公司的任务调度进行管理等。
下面提供一个拿去就能用的配置文件,改一下数据源、用户名和密码即可:
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=20
org.quartz.threadPool.threadPriority=5
org.quartz.jobStore.misfireThreshold=60000
org.quartz.jobStore.class= org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=qrtz_
org.quartz.jobStore.dataSource=qzDS
org.quartz.dataSource.qzDS.driver= com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL= jdbc:mysql://localhost:3306/testDB
org.quartz.dataSource.qzDS.user= root
org.quartz.dataSource.qzDS.password= admin
org.quartz.dataSource.qzDS.maxConnection= 20
2、JobStoreCMT
CMT的全称是Container Managed Transactions,表示容器管理事物,也就是让应用容器托管事物。这里假设应用容器是Tomcat,并且项目和Quartz都是使用Tomcat配置的数据源,那么项目和Quartz的代码中就可以共用同一个事物,不管是业务代码还是Quartz内部抛出异常,Service服务内的所有数据操作都会回滚到原始状态。JobStoreCMT和JobStoreTX最大的区别是JobStoreCMT需要配置两个数据源,一个是受应用容器管理的数据源,还有一个是不受应用容器管理的数据源。
这里需要想一想为什么需要两个数据源?
我个人的理解是不受应用容器管理的数据源用来由Quartz内部进行"增删改查",假如一个触发器已失效,那么Quartz框架内部就会自动删除这个触发器并提交事物,而无需开发人员的项目代码来处理,全由Quartz内部管理。
下面介绍一下使用JobStoreCMT配置步骤,所有的配置都是在quartz.properties中完成:
- 第一步配置org.quartz.jobStore.class属性:
org.quartz.jobStore.class= org.quartz.impl.jdbcjobstore.JobStoreCMT
- 第二步配置驱动代理,以Mysql为例,其它代理类参考上面表格:
org.quartz.jobStore.driverDelegateClass= org.quartz.impl.jdbcjobstore.StdJDBCDelegate
- 第三步配置两个数据源:
第一个:配置不受应用容器管理的数据源:
org.quartz.jobStore.nonManagedTXDataSource = qzDS
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/testDB
org.quartz.dataSource.qzDS.user = root
org.quartz.dataSource.qzDS.password = admin
org.quartz.dataSource.qzDS.maxConnections = 10
nonManagedTXDataSource就是非管理事物数据源的意思。
第二个:配置受应用容器管理的数据源:
org.quartz.dataSource.dataSource=myDS
org.quartz.dataSource.jndiURL = jdbc/mysql
org.quartz.dataSource.myDS.jndiAlwaysLookup = DB_JNDI_ALWAYS_LOOKUP
org.quartz.dataSource.myDS.java.naming.factory.initial = org.apache.naming.java.javaURLContextFactory
org.quartz.dataSource.myDS.java.naming.provider.url = http://localhost:8080
org.quartz.dataSource.myDS.java.naming.security.principal = root
org.quartz.dataSource.myDS.java.naming.security.credentials = admin
注意:配置之前大家可能需要去了解学习一下JNDI+应用容器(Tomcat等)如何配置数据源,本文就不讲述如何配置了。
下面解释一下受应用容器管理的数据源配置属性的含义:
- org.quartz.dataSource.NAME.jndiURL
受应用服务器管理的DataSource的JNDI URL - org.quartz.dataSource.NAME.java.naming.factory.initial
JNDI InitialContextFactory的类名称 - org.quartz.dataSource.NAME.java.naming.provider.url
连接到JNDI的URL - org.quartz.dataSource.NAME.java.naming.security.principal
连接到 JNDI 的用户名 - org.quartz.dataSource.NAME.java.naming.security.credential
连接到 JNDI 的用户凭证密码
(三)、如何选择使用哪种存储机制?
1、什么情况下使用RAMJobStore内存存储方式呢?
根据开发中的使用经验,发现有些任务是随着项目启动而启动的,就算项目关闭或系统宕机,那也没关系,因为项目重新启动后此任务又会随之启动。如果项目中只存在这类任务,那么就可以用内存存储。随着项目启动有几种常用的实现方式,第一种是通过实现ServletContextListener监听器接口,然后在接口实现类的contextInitialized()方法中编写启动Job的硬编码;第二种是通过Quartz的XML配置文件启动任务。
2、什么情况下使用JobStoreTX数据库存储方式呢?
第一篇文章@一、Quartz集成-下载和安装中的配置就用到了JobStoreTX,那个配置文件是我在实际开发中使用的,使用这种存储方式的情况很多。使用这种方式需要注意的是,如果在一个Service服务中需要创建一个Job,那么请把创建Job的代码编写在服务代码的最后面,确保业务代码运行成功并且没有抛异常再去启动Job,如果启动Job失败的时候请抛出一个运行时异常使业务代码进行回滚。
例子:
@Transactional
public void demoService(TaskStore taskStore) {
// 先执行插入业务操作
taskStoreService.insert(taskStore);
// 再执行更新业务操作
taskDetailService.update(taskDetail);
// 最后启动定时任务
QuartzUtils.addJob("testName", DemoJob.class, "0 * * * * * ?");
}
注意例子中的addJob()方法中捕获了异常后进行重新封装再抛出运行时异常的,目的是Quartz内部错误时确保业务代码回滚。
3、什么情况下使用JobStoreCMT数据库存储方式呢?
JobStoreCMT和JobStoreTX的区别前文已经介绍了,在实际开发的过程中我还没有在项目中使用过此种方式。一般情况下都是使用的JobStoreTX。如果大家的项目中有着严格的事物管理,那么建议使用JobStoreCMT存储方式。