默认情况下:
- Quartz 任务都是保存在内存中的,因此在集群环境下,不可避免的会出现相同的任务同时执行的情况;
- 如果在 Config.groovy 中启用 quartz.jdbcStore 将任务保存到数据库,并设置任务(XxxxJob)的属性 concurrent 为 false,则可以支持集群环境下不允许同时运行多个相同的任务;
- 进一步,如果多个应用设置不同的实例名称(quartz.props.scheduler.instanceName),则可以支持同名应用的集群不允许同时运行多个相同的任务,而不同应用的集群可以同时运行相同的任务。
但是,在实际运行过程中,效果却不甚理想,总是出现一些莫名的错误。
所以在常规的 J2EE 应用开发中,通常是把 quartz 任务单独作为一个 war 包部署,甚至部署到独立的 jvm 中,以避免各种冲突。
BroFramework 中提供了一套基于数据库的Lock API,使用这套API,可以灵活的处理 Quartz 中的任务执行冲突,并且实现各种 Quartz 原本无法实现的策略。
考虑下面的几个场景中,应用 Foo 和应用 Bar 中,有一个同名的 FoobarJob,这两个应用使用同一套 BroFramework 数据库:
1、希望同时只有一个 FoobarJob 执行:
import bropen.framework.core.Locker;
class FoobarJob {
/**
* 执行定时任务
*/
def execute() {
String lockKey = "FoobarJob"; // 按任务名加锁
if ( !Locker.lockExit(lockKey, false) ) return; // 加锁失败则直接退出
try {
// 任务执行代码
} finally {
Locker.unlock(lockKey, false); // 执行完毕,解锁
}
}
}
2、希望 Foo 和 Bar 的多个集群节点中只有一个 FoobarJob 执行,但是Foo、Bar两个应用可以同时运行 FoobarJob
import bropen.framework.core.Locker;
import bropen.framework.core.security.DomainApplication;
class FoobarJob {
/**
* 执行定时任务
*/
def execute() {
String lockKey = "FoobarJob_" + DomainApplication.current().id; // 按任务名和应用加锁
if ( !Locker.lockExit(lockKey, false) ) return; // 加锁失败则直接退出
try {
// 任务执行代码
} finally {
Locker.unlock(lockKey, false); // 执行完毕,解锁
}
}
}
3、如果有多个域(启用域管理后),可以在 lockKey 中添加域的ID(Domain.current().id)做为锁的关键字,以实现一个域的多个应用中,只允许运行一个同名的任务。
4、如果有两个任务会争用相同的资源,可以为两个任务设置相同的 lockKey,并且可以使用 lock 方法来代替 lockExit 方法,实现等待功能,如:
Locker.lock(lockKey, false, null, -1) // 一直等待
5、如果 FoobarJob 执行时,有自动发出邮件、生成待办等操作(比如每周三凌晨,给所有员工发出一个工作周报的待办任务),而且执行时长不等、执行间隔比较长(如大于半个小时),则在 execute 的 finally 中不解锁,而是让锁自动超时失效,以避免集群间极小的时间差导致任务多次重复执行,如:
def execute() {
String lockKey = "FoobarJob"; // 按任务名加锁
if ( !Locker.lockExit(lockKey, false, 30*60*1000) ) return; // 加锁失败则直接退出,否则加锁并设置超时时间为 30 分钟(默认)
// 任务执行代码,执行完后不解锁,让其自动超时失效
....
}