Quartz Job Scheduling Framework[翻译]第六章. Job 存储和持久化(第二部分)

译者:Unmi(隔叶黄莺)
http://www.blogjava.net/Unmi/archive/2008/05/31/204228.html

八. 为 JobStroreTX 创建数据源

当使用持久性 JobStore 时,Quartz 需要一个数据源。数据源扮演着产生数据库连接工厂的角色。在 Java  中,所有的数据源要实现 java.sql.Datasource 接口。Quartz 自身并不提供 Datasource 的所有功能;它只代表了那一职责。默认的,Quartz 能使用另一开源的框架,叫做 Commons DBCP,或者可以通过 JNDI 查找应用服务器中定义的 DataSource。

DBCP 是一个 Jakarta Commons 项目,网址是 http://jakarta.apache.org/commons/dbcp。这个框架的二进制版包含在 Quartz 的发行版中,你应该把它加到你的 Quartz 应用中来。你还需要加入 Commons Pool 库,它也包含中 Quartz 发行版中,是 DBCP 要用到的。

使用 JobStoreTX 时,你必须在 quartz.properties 文件中指定 Datasource 属性。这允许 Quartz 为你创建并管理 Datasource。表 6.4 列示了使用 JobStoreTX 时需要的 Datasource 配置属性。

表 6.4. 配置 Quartz Datasource 的可用属性

属性 必须
org.quartz.dataSource.NAME.driver 是
描述:JDBC 驱动类的全限名

org.quartz.dataSource.NAME.URL 是
描述:连接到你的数据库的 URL(主机,端口等)

org.quartz.dataSource.NAME.user 否
描述:用于连接你的数据库的用户名

org.quartz.dataSource.NAME.password 否
描述:用于连接你的数据库的密码

org.quartz.dataSource.NAME.maxConnections 否
描述:DataSource 在连接接中创建的最大连接数

org.quartz.dataSource.NAME.validationQuary 否
描述:一个可选的 SQL 查询字串,DataSource 用它来侦测并替换失败/断开的连接。例如,Oracle 用户可选用 select table_name from user_tables,这个查询应当永远不会失败,除非直的就是连接不上了。


表 6.4 中列出的每一个属性,你需要用你选择的名字替换掉属性的 NAME 部分。只要保证 Datasource 的所有属性的 NAME 部分相同就行了。这个名字用于唯一的标识 Datasource。假如你需要配置多个 Datasource (在使用 JobStoreCMT 时你将会这么做),每一个 Datasource 应该有一个唯一的 NAME 值。

代码 6.2 展示了一个为 JobStoreTX 配置 Datasource 的例子,它需要加到 quartz.properties 文件中

代码 6.2. 一个用于非 CMT 环境的 Quartz Datasource 的例子

org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver   
org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz   
org.quartz.dataSource.myDS.user = admin   
org.quartz.dataSource.myDS.password = myPassword   
org.quartz.dataSource.myDS.maxConnections = 10  
org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz
org.quartz.dataSource.myDS.user = admin
org.quartz.dataSource.myDS.password = myPassword
org.quartz.dataSource.myDS.maxConnections = 10

像上面代码 6.2 那样加入了 Datasource 部分到 quartz.properties 文件后,你仍然需要使之对于已配置的 Quartz JobStoreTX 是可用的。你可以通过把下面的属性加到属性文件中来做到这一点:

org.quartz.jobStore.DataSource = <DS_NAME>

这个 <DS_NAME> 应该与指定给个 Datasource 配置的名字相匹配。对于代码 6.2 中的例子来使用 Datasource,你应当在 quartz.properties 文件中加入下面这行:

org.quartz.jobStore.dataSource = myDS

这个值然后会传递给 JobStoreSupport 并且对于你的 JobStoreTX 就可用了,这样连接就可以被获取并传递到 DriverDelegate 实例。

九. 应用 JobStoreTX 运行 Quartz

当你已完成前面的配置步骤时,你的程序就可以准备启动了。正如前面所有的例子那样,你仍然需要一个启动类来从工厂创建一个 Scheduler 实例,并调用它的 start() 方法。一个如代码 6.3 中的类就足够了。

代码 6.3. 简单的启动类,从命令行调用来启动 Scheduler

 
public class SchedulerMain {
  static Log logger = LogFactory.getLog(SchedulerMain.class);
  public static void main(String[] args) {
    SchedulerMain app = new SchedulerMain();
    app.startScheduler();
  }
  public void startScheduler() {
    try {
      // Create an instance of the Scheduler
      Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
      logger.info("Scheduler starting up...");
      scheduler.start();
    } catch (SchedulerException ex) {
      logger.error(ex);
    }
  }
}

当你使用代码 6.3 中的 SchedulerMain 类来测试 JobStoreTX 配置,你将得到类似于如下那样的输出:

INFO [main] - Quartz Scheduler v.1.5.0 created.
INFO [main] - Using thread monitor-based data access locking (synchronization).
INFO [main] - Removed 0 Volatile Trigger(s).
INFO [main] - Removed 0 Volatile Job(s).
INFO [main] - JobStoreTX initialized.
INFO [main] - Quartz scheduler 'QuartzScheduler' initialized from default resource
file in Quartz package: 'quartz.properties'
INFO [main] - Quartz scheduler version: 1.5.0
INFO [main] - Scheduler starting up...
INFO [main] - Freed 0 triggers from 'acquired' / 'blocked' state.
INFO [main] - Recovering 0 jobs that were in-progress at the time of the last
shut-down.

INFO [main] - Recovery complete.
INFO [main] - Removed 0 'complete' triggers.
INFO [main] - Removed 0 stale fired job entries.
INFO [main] - Scheduler QuartzScheduler_$_NON_CLUSTERED started.

日志信息是用 Log4J 显示的,因而它们可能和你实际的输出略有差别。一些事情从输出来看是很明显的。首先,在数据库中没有发现 Trigger 或 Job。这是很重要的,有些时候也是使人困惑之处。用了数据库却未给你加载任何的 Job 或 Trigger:是这样的,因为它无法知道谁来为你加载。这是你自己不得不做的事情,你可以几种方式把 Scheduler 信息存入到数据库中。


十. 使用数据库存储 Scheduler 信息

·加载 Job 到数据库中

在前面有一节,"使用内存存储 Scheduler 信息",我们谈到关于在使用 RAMJobStore 时如何加载 Job 和 Trigger 信息到内存中。那么 Job 和 Trigger 又是如何加载到数据库中的呢?存在以下几个方法把 Job 信息存入到数据库:

· 在你的程序中加入 Job 信息
· 使用 JobInitializationPlugin
· 使用 Quartz Web 应用程序

我们在前面的 RAMJobStore 章节中讨论过前面两种途径。当它们用于 JDBC JobStore 时,并没有多大不同,只些许例外。首先,你需要知道,当使用这两个方法式,Job 信息是在数据库中的。甚至在你停止了程序后,这些信息仍然保留在数据库中。甚至是你不在你的程序中使用 JobInitializationPlugin 时,这些信息也还在数据库中。基于这一点,它是会从数据库中找寻 Job 信息。第八章涵盖了 JobInitializationPlugin 和常用的 Quartz 插件。

最后一种方法可能是最有意思的。我们还没有谈论到 Quartz Web 应用,但是我们在第十三章 "Quartz 和 Web 应用" 是这么做的。在现在呢,你应当知道 Quartz Web 应用是一个基于浏览器的 GUI 程序,它是设计用来管理 Quartz Scheduler。它是由 Quartz 用户设计的,它为加入 Job 和 Trigger、启动和暂停 Scheduler 和发布其他功能呈现了一个相当好的界面。

通过 SQL 工具加载 Job
最后还有一种能用于加载 Job 信息的方法,但仅在这儿提一下,并不鼓励你去尝试它。这种方法是使用本地 SQL 直接操作 Quartz 表来尝试加载和/或修改信息。使用本地查询工具来加入 Job 信息到数据库中只在少数时候这样做,但是很容易破坏数据进而导致所有 Job 不能正确运行。无论如何应尽力避免用这种方法。


十一. 使用 JobStoreCMT

许多我们在前面章节对 JobStoreTX 所说的和做的对于另一版本的 JDBC JobStore,JobStoreCMT 来也是适用的。再说,也没说所有,这不足为奇,因为它们都是 JobStore 类型,它们都是设计成用 JDBC 来与关系型数据库交互。也都是继承自共同的基类。

JobStoreCMT 被设计成参与到容器的事物边界内。这意味着容器创建一个 JTA 事物并使之对于 JobStore 可用。Quartz 与 JobStore 的交互保持在这个事物中。假如出现任何问题,Quartz 能给容器一个信号,它希望通过调用事物的 setRollbackOnly() 使事物回滚。

·配置 JobStoreCMT

同之前的 JobStoreTX 和 RAMJobStore 一样,要使用 JobStoreCMT 的第一步是告知 Scheduler 你打算用 JobStoreCMT。和以前类似,也是通过在 quartz.properties 文件中设置 JobStore 类属性来做到这一点的:

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT

如果属性文件中存在 RAMJobStore 行,要确保移除了它。

·配置 DriverDelegate 类

你也是需要像为 JobStoreTX 所做的那样选择 DriverDelegate。Quartz 依靠一个 DriverDelegate 与给定的数据库通信。代理负责了与 JDBC Driver,也就是数据库的所有通信。

回到表 6.2 中的 DriverDelegate 列表,并基于你的数据库平台和环境选择一个。要加 MS SQLServer 代理到 quart.properties 文件,那就加入下一行:

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.MSSQLDelegate 


你可以使几个属性来帮助调整 JobStoreCMT。表 6.5 列举了全部设置项。

表 6.5. 用于设置 JobStoreCMT 的配置属性

属性 默认值
org.quartz.jobStore.driverDelegateClass
描述:能够理解不同数据库系统中特定方言的驱动代理

org.quartz.jobStore.dataSource
描述:这是一个用于 quartz.properties 文件的数据源配置块的名字。

org.quartz.jobStore.nonManagedTXDataSource
描述: JobStoreCMT 需要一个(第二个) 数据源,它所包含的连接不作为容器管理事特的一部分。 这个属性值必须是一个定义在配置属性文件中的数据源的名字。这个数据源必须包含非容器管理事物(non-CMT) 连接,换名话说就是, 它产生的连接可让 Quartz 直接合法的调用它的 commit() 和 rollback() 方法。

org.quartz.jobStore.tablePrefix QRTZ_
描述:这是指定给 Scheduler 的一套数据库表名的前缀。Schedulers 在指定了不同前缀时可在同一数据库中使用不同的表。

org.quartz.jobStore.useProperties False
描述:"use properties" 标记指示着持久性 JobStore 所有在 JobDataMap 中的值都是字符串,因此能以 名-值 对的形式存储,而不用让更复杂的对象以序列化的形式存入 BLOB 列中。这样会更方便,因为让你避免了发生于序列化你的非字符串的类到 BLOB 时的有关类版本的问题。

org.quartz.jobStore.misfireThreshold 60000
描述:在 Trigger 被认为是错过触发之前,Scheduler 还容许 Trigger 通过它的下次触发时间的毫秒数(译者注:据原文翻译,真的不好理解,实际效果可参看:http://www.blogjava.net/Unmi/archive/2007/10/23/153413.html 我在评论中的实验)。默认值(假如你未在配置中存在这一属性条目) 是 60000(60 秒)。这个不仅限于 JDBC-JobStore;它也可作为 RAMJobStore 的参数

org.quartz.jobStore.isClustered False
描述:设置此为 true 来打开集群特性。假如你有多个 Quartz 实例使用同一套数据库表时这个属性必须设置为 true。

org.quartz.jobStore.clusterCheckinInterval 15000
描述:设置一个频度(毫秒),用于实例报告给集群中的其他实例。这会影响到侦测失败实例的敏捷度。它只用于设置了 isClustered 为 true 的时候。

org.quartz.jobStore.maxMisfiresToHandleAtATime 20
描述: 这是 JobStore 能处理的错过触发的 Trigger 的最大数量。处理太多(超过两打) 很快会导致数据库表被锁定够长的时间,这样就妨碍了触发别的(还未错过触发) trigger 执行的性能。

org.quartz.jobStore.dontSetAutoCommitFalse False
描述:设置这个参数为 true 则告诉 Quartz 不要调用从 DataSource 获取到的连接的 setAutoCommit(false) 方法. 这在少些情况下是有帮助的、例如你有一个驱动在已是 off 时又调用了这个方法会有所抱怨. 这个属性默认为 false,因为多数驱动需要调用 setAutoCommit(false) 方法。

org.quartz.jobStore.selectWithLockSQL SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE
描述:这必须是一个从 LOCKS 表查询一行并对这行记录加锁的 SQL 语句。假如未设置,默认值就是 SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE,这能在大部分数据库上工作。{0} 会在运行期间被前面你配置的 TABLE_PREFIX 所替换。

org.quartz.jobStore.dontSetNonManagedTX
ConnectionAutoCommitFalse False
描述:这个属性同 org.quartz.jobStore.dontSetAutoCommitFalse, 只是它还可以应用于不受管理事物的数据源(nonManagedTXDataSource)。

org.quartz.jobStore.txIsolationLevelSerializable False
描述:值为 True 时告诉 Quartz (当使用 JobStoreTX 或 CMT 时) 调用 JDBC 连接的 setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 方法。这能助于防止某些数据库在高负荷和长事物时的锁超时。

org.quartz.jobStore.txIsolationLevelReadCommitted False
描述:当设置为 true 时,这一属性告诉 Quartz 调用不受管理的 JDBC 连接的 setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED) 方法。这能助于防止某些数据库(如 DB2) 在高负荷和长事物时的锁超时。


十二. 为 JobStoreCMT 配置数据源

跟 JobStoreTX 一样,我们需要配置一个 Datasource 才能使用 JobStoreCMT。然而,JobStoreCMT 需要两个 Datasource 而不是像 JobStoreTX 只要一个。其中一个 Datasource 和我们为 JobStoreTX 设置的类同:作为不受管理的数据源。同时呢,我们还需配置第二个数据源,是作为受管理的数据源,它由应用服务器来进行管理。

为什么 JobStoreCMT 需要两个 Datasource 呢?

JobStoreCMT 的原始作者,Jeffrey Wescott,设计 JobStoreCMT 使用一个标准的 JDBC 连接来做它“自己的工作”,同时,代表客户端(如部署 Job) 的工作在执行时是使用一个在容器控制之下有自身事物的 JDBC 连接。即使 Quartz 处在一个大事物中,这种设计也允许用户与 Quartz 交互,而无需 JobStoreCMT 非得使用应用服务器的事物管理器(例如,经由 UserTransaction) 在做自己内部工作时(如处理已错过执行的 Trigger) 来创建和终止事物。如果是 JobStoreCMT 使用 UserTransation 只给它配置一个数据源,从配置方面来看确实方便。然而,在相比于别的特性需求和改进的必要性时,作此变化并不会成为团队中首要的问题,因而 JobStoreCMT 还是继续要两个数据源。


·配置不受管理的数据源

我们在设置不受管理的数据源的多数操作与为 JobStoreTX 所做是相同的,只是我们还要加上一行来指定这是 nonManagedTXDataSource:

# Add the property for the nonManagedTXDataSource
org.quartz.jobStore.nonManagedTXDataSource = myDS

org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz
org.quartz.dataSource.myDS.user = admin
org.quartz.dataSource.myDS.password = myPassword
org.quartz.dataSource.myDS.maxConnections = 10


这是配置不受管理的数据源,并让 JobStore 知道这个 nonManagedTXDataSource 叫做 "myDS"。

·配置受管理的 Datasource

第二个数据源需配置为一个受管理的 Datasource。这意味着 Quartz 在执行 Scheduler 操作时使用一个容器已创建好的 Datasorce 与数据库交互。当 Quartz 从 Datasource 上取得了连接后,在 Quartz 部署 Job 和 Trigger 时应有一个 JTA 事物。例如,代码要求 Quartz 在 SessionBean 的一个方法上的事物描述符设置为 REQUIRED。另一个应用是客户端程序要通过使用 javax.transaction.UserTransaction 直接启动一个事物。

和不受管理的 Datasoure 一样,也是要在 quartz.properties 文件中配置容器管理的 Datasource。下面的例子描述了如何设置受管理的 Datasource:

org.quartz.dataSource.NAME.jndiURL=jdbc/quartzDS

org.quartz.dataSource.NAME.java.naming.factory.initial=weblogic.jndi.WLInitialContextFactory

org.quartz.dataSource.NAME.java.naming.provider.url=t3://localhost:7001
org.quartz.dataSource.NAME.java.naming.security.principal=weblogic
org.quartz.dataSource.NAME.java.naming.security.credentials=weblogic


表 6.6 列出了受管理的 Datasource 可用的属性。

表 6.6. 在应用服务器上所用的 Datasource 的属性

属性 必须
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 上下文的用户主体(Unmi 注:用户名)

org.quartz.dataSource.NAME.java.naming.security.credential 否
描述:可选项,连接到 JNDI 上下文的用户凭证(Unmi 注:密码)

使用到表 6.6 中的属性,这儿有一个在 quartz.properties 中配置受管理的 Datasource 的例子。
org.quartz.dataSource.WL.jndiURL = OraDataSource
org.quartz.dataSource.WL.jndiAlwaysLookup = DB_JNDI_ALWAYS_LOOKUP
org.quartz.dataSource.WL.java.naming.factory.initial = weblogic.jndi.WLInitialContextFactory
org.quartz.dataSource.WL.java.naming.provider.url = t3://localhost:7001
org.quartz.dataSource.WL.java.naming.security.principal = weblogic
org.quartz.dataSource.WL.java.naming.security.credentials = weblogic


------------------------------------------------
[Unmi 后记]
又半月有途未动笔墨了,不过仍然保持着只要有机会上网就一定到 BlogJava 来阅读最新本章的习惯。部门人员锐减,事情也就杂乱不堪起来,对于开发技术的选择上客观上完全能由我自行决断,没有人与我争议了,甚是悲凉。


起初对 Quartz 这个系列的翻译只为一时之兴,一路过来翻译工作其实蛮费时间的。一篇英文阅读完之后,还需花费几近于 20 倍的时间才能用中文记录下来。因为阅读总是眼观六路,一知半解的,完全转换成中文就要字句斟酌了。

翻译进行到这个阶段,我当然还会继续坚持,从其中获得的好处也是不言而喻的。主要表现在两方面:

1. 对技术把握的更精细。阅读是放眼而瞟,只求个大概;翻译则不同,本身未能理解个相当,何以能用中文向他人解译的清楚呢?蕴责任于其中。对于多数例子,并非照搬了事,都有再次测试感受过的。译章置于网上之后,亦有许多朋友就 Quartz 使用时的疑问,本人也会带着某种责任心,尽我能力作解答,也非常有助于自身对该项技术的掌握。

2. 阅读与翻译的速度提升也是显而易见的。最初时的每字每句的爬梳,须频繁请求各方资源才能完成一篇,现在与那时相比,可谓顺畅多了。许多篇章纵使离开英文词典也无碍了。用数据来说吧,现在翻译一篇的速度大概是以前的四至五倍。以后的前行中需要面对更多的英文资料,通过对 Quartz 这个手册翻译扎实锤炼了自己的英文阅读能力,写作能力亦在其内。

之于以上两点能对我产生的影响,尤其是 Quartz 手册的翻译已完成大部分时,我一定还会继续完成它,也非常感谢网络上各位朋友们的支持。


十三. 改善持久性 JobStore 的性能

当在只有最少量时间做任何相关事情的时候,性能是一个广受人瞩目的课题。作为有经验的开发者,我们知道从项目之初它就成为一个需要考虑的事。

在使用 Quartz 的 JobStore 时,最大的关注面就是有关于与关系型数据库的交互。数据库 I/O(就像文件 I/O) 通常不是很快。你可以通过采取一些措施,如调优 SQL、增加索引和操作表和列等来改善性能。因为性能问题在写 Quartz 框架的时候就已有考虑到,而你又不想在未出现实际的性能问题时扎入到 Quartz 中做些手脚,那么可试图通过配置来解决它,或者尝试所有可能的方式,只要不是维护源代码。最好的消息是 Quartz 是开源的,你完全可以窥入其中了解它做了什么和如何实现的。假如你不喜欢它现有的查询数据库的方式,你有权去修正它。不过,在你采取行动之前,确定检查了 Quartz 论坛上的用户和开发者,看看是否其他人也遇到了相关的问题并浏览推荐的做法。

一个很简单(也是很有效的) 改善性能的方式是确保在所有适当的列上创建了索引。Quartz 所带的某些数据库创建脚本已有创建索引的命令。如果你的没有,你可以简单参考定义在 tables_oracle_sql 底端的语句并针对你的 RDBM 需要作些语法上改变。

不管你怎么做的,假如你修改了 Quartz 来改善性,一定要反馈到社区和 Quartz 项目。

十四. 创建新的 JobStore

对于多数用户,JobStore 所提供开箱即用的实现已是足够了。当你的应用在重启之间不需要维护状态,那么 RAMJobStore 就是你的第一选择。它速度快,易于配置,也不会带来什么麻烦。另一方面,如果你需要在重启之间维护 Scheduler 的状态,且正使用一个数据库或可以访问数据,那么用一个 JDBC JobStore 或许是你最后的选择。

那会在什么时候你需要一个完全不同的 JobStore 类型?你将需要创建一个新的类型。本节讨论几种途径,并针对如何创建一个新的 JobStore ,在所提供的方案无法满足你时给出一些主意。

·实现 JobStore 接口

无论它们是否是用的数据库、文件系统、甚至是内存,所有的 JobStore 必须实现 JobStore 接口。你创建的新的 JobStore 也不例外。回头看本章前面部分,你会发现 RAMJobStore 直接实现了 JobStore 接口,JDBC JobStore 是 JobStroeSupport 的子类,它本身实现了 JobStore 接口。

JobStore 接口有 40 个方法,它要求任何 JobStroe 实现都必须实现这些方法,你的也一样。你如何实现那些方法完全依赖于你正构建的 JobStore 的类型。那不是说你的 JobStore 将只能有 40 个方法;这仅仅是接口需要的最小数量。这 40 个方法体现 JobStore 和 Scheduler 之间的公共契约。

让我们拣出其中一个方法来简短的讨论它。我们就选 JobStore 接口方法:

public void schedulerStarted() throws SchedulerException;


Scheduler 调用 JobStore 的 SchedulerStarted() 方法去通知 JobStore Scheduler 已经启动了。如果你看了 RAMJobStore 的实现,你能发现它在这个方法实现中什么也没做:

public void schedulerStarted() throws SchedulerException{
    // nothing to do
}


然而,假如你去看那两个 JobStore 的实现,你会看到在 Scheduler 在首次启动时进行了一些工作:

 
  public void schedulerStarted() throws SchedulerException {
    if (isClustered()) {
      clusterManagementThread = new ClusterManager(this);
      clusterManagementThread.initialize();
    } else {
      try {
        recoverJobs();
      } catch (SchedulerException se) {
        throw new SchedulerConfigException("Failure occurred during job recovery.", se);
      }
    }
    misfireHandler = new MisfireHandler(this);
    misfireHandler.initialize();
  }
 

每一个 JobStore 实现会是唯一的,在接口方法内部实现的功能也是不同的。如果你是认真的去创建你自己的 JobStore,你就应当好好看看 RAMJobStore 的源代码来完全理解 JobStore 所有职责。RAMJobStore 应该作为你需要定制任何 JobStore 时的指南。
快乐渡过每一天,减肥坚持每一天