文章目录
前言
开发的过程中经常会遇到需要定时器的功能,例如:
- 每天晚上同步用户信息
- 每隔一段时间计算用户喜欢的类型
- 定时发送邮件
- 定时发送短信
等等。定时的框架也很多,但是较为轻量级的quartz一定可以占据一席之地。
quartz是一个相对轻量级的定时器框架,可以在分布式环境中运行定时任务。在此,不讲如何使用quartz框架,更加着重阐述quartz框架如何更好的保证任务的运行。
一、Job Stores
一个任务的执行时间,执行时需要调用哪个方法,都是需要保存的。Job Stores直译为工作存储,在quartz框架中是任务的保存方式。常见的有两种RAMJobStore(内存保存)和JDBC JobStore(数据库保存)。
- RAMJobStore:是使用最简单的JobStore,它也是性能最高的(在CPU时间方面)。它将其所有数据保存在RAM中。这就是为什么它是闪电般快的,也是为什么这么简单的配置。缺点是当您的应用程序结束(或崩溃)时,所有调度信息都将丢失 - 这意味着RAMJobStore无法履行作业和triggers上的“非易失性”设置。对于某些应用程序,这是可以接受的 - 甚至是所需的行为,但对于其他应用程序,这可能是灾难性的。
- JDBC JobStore:它通过JDBC将其所有数据保存在数据库中。因此,配置比RAMJobStore要复杂一点,而且也不是那么快。但是,性能下降并不是很糟糕,特别是如果您在主键上构建具有索引的数据库表。在相当现代的一套具有体面的LAN(在调度程序和数据库之间)的机器上,检索和更新触发triggers的时间通常将小于10毫秒。尽管有这么多的不好,但是这种方式可以保证任务以安全的方式运行。在创建定时任务时,也许不需要多高的效率,只需要保证任务稳定执行即可。
一种是内存的,一种是数据库的,肯定还有中间版本,基于缓存的。TerracottaJobStore就是介于两者之间的一种存储介质。这种方式,可能用的少吧,有兴趣的可以百科一下。
1.1、JDBC JobStore 详解
Quartz的集群功能通过故障转移和负载平衡功能为您的调度程序带来了高可用性和可扩展性。
如上任务,简单的定时任务框架,如果在执行定时任务的时候,每个服务都会执行一次。另外,如果想在多服务下通过master选举的方式保证任务只执行一次的话,会有很多弊端,比如:所有的任务都在一个服务执行,造成任务运行缓慢,卡顿等现象。
在如上这种架构时,quartz框架可以实现任务稳定的执行,并且任务的分配相对均衡(每天凌晨7点发送短信,可能会在服务A执行,每天8点发送头条新闻,可能会在服务B执行);可以实现故障转移(某个任务在执行的过程中挂掉,另一个服务会重新执行)。
二、思路
思路如下
第一步
查询qrtz_triggers表
获取将要执行的触发器,将他认定为马上要执行(插入qrtz_fired_triggers
表一条记录)。QuartzSchedulerThread
中搜索acquireNextTriggers
方法可见。
默认没有加悲观锁,但是可以通过配置使用悲观锁。,其中的操作有查询qrtz_triggers表,修改qrtz_triggers表,和插入qrtz_fired_triggers表,3次数据库操作,3次在同一个事务中。而且修改的时候也只修改状态为WAITING的数据,修改成功才插入,这样可以避免集群中插入重复数据。这样总感觉很是麻烦,为什么这么处理,下一篇说明。
第二步
获取马上要执行的触发器,修改状态为执行中。triggersFired
方法可见。
将第一步插入的qrtz_fired_triggers
的数据修改为EXECUTING
状态。此过程由悲观锁保证线程安全。
第三步
如果检验通过,则执行任务,执行完成,修改qrtz_triggers
表记录,并且删除qrtz_fired_triggers
表记录。JobRunShell .run
方法可见。
执行对应的方法,因为已经执行完成,把qrtz_triggers
表的数据修改为WAITING
状态,并且删除qrtz_fired_triggers
表的数据。与数据库交互的过程由悲观锁保证线程安全。
三、 源码解析
核心代码QuartzSchedulerThread.run
方法。
QuartzSchedulerThread.run入口方法
//这个是线程启动的方法,当调用scheduler.start()的时候这里就开始执行了 。
public void run() {
int acquiresFailed = 0;
while (!halted.get()) {
// 这里锁的概念,说实话挺乱的,没太在意。
try {
//省略部分代码
//如果当前空余线程数大于0
if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...
List<OperableTrigger> triggers;
long now = System.currentTimeMillis();
clearSignaledSchedulingChange();
try {
//查询qrtz_triggers表并且返回触发器信息。
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
acquiresFailed = 0;
if (log.isDebugEnabled())
log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
}
//省略部分代码
if (triggers != null && !triggers.isEmpty()) {
now = System.currentTimeMillis();
long triggerTime = triggers.get(0).getNextFireTime().getTime();
long timeUntilTrigger = triggerTime - now;
//这个循环里面考虑了:在等待任务时间到达的过程中,
//Scheduler状态发生变化,
//例如,又新加了一个新的任务,新的任务执行时间可能比目前等待执行的时间早,
//此时就应该考虑新加入的任务。
//所以作者认为,只有去获取新trigger所花费的时间小于马上执
//行还剩的时间的时候,才会抛弃当前trigger,去选择新来的trigger。
//有点绕,这算是一种补偿机制吧。
while(timeUntilTrigger > 2) {
synchronized (sigLock) {
if (halted.get()) {
break;
}
if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
try {
// we could have blocked a long while
// on 'synchronize', so we must recompute
now = System.currentTimeMillis();
timeUntilTrigger = triggerTime - now;
if(timeUntilTrigger >= 1)
sigLock.wait(timeUntilTrigger);
} catch (InterruptedException ignore) {
}
}
}
if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
break;
}
now = System.currentTimeMillis();
timeUntilTrigger = triggerTime - now;
}
if(goAhead) {
try {
//将触发器修改为执行中
//稍后有详细说明
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
if(res != null)
bndles = res;
} catch (SchedulerException se) {
qs.notifySchedulerListenersError(
"An error occurred while firing triggers '"
+ triggers + "'", se);
//QTZ-179 : a problem occurred interacting with the triggers from the db
//we release them and loop again
for (int i = 0; i < triggers.size(); i++) {
qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
}
continue;
}
}
for (int i = 0; i < bndles.size(); i++) {
TriggerFiredResult result = bndles.get(i);
TriggerFiredBundle bndle = result.getTriggerFiredBundle();
Exception exception = result.getException();
if (exception instanceof RuntimeException) {
getLog().error("RuntimeException while firing trigger " + triggers.get(i), exception);
qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
continue;
}
// it's possible to get 'null' if the triggers was paused,
// blocked, or other similar occurrences that prevent it being
// fired at this time... or if the scheduler was shutdown (halted)
if (bndle == null) {
qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
continue;
}
//zhix
JobRunShell shell = null;
try {
//执行任务,详细在 JobRunShell 的run方法中
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);
}
}
continue; // while (!halted)
}
} else { // if(availThreadCount > 0)
// should never happen, if threadPool.blockForAvailableThreads() follows contract
continue; // while (!halted)
}
//省略部分代码
} // while (!halted)
}
3.1、 acquireNextTriggers方法
acquireNextTriggers
方法意为获取将要执行的触发器。
- idleWaitTime 默认30秒,意为获取将来30秒要执行的触发器。
- availThreadCount空闲线程数量。
- getMaxBatchSize 每一次拉取触发器的数量,默认为1。
- BatchTimeWindow 时间窗口参数,默认为0。
这样,在此即可简单认为,获取一个将来30秒内要触发的触发器。
3.1.1、JobStoreSupport.acquireNextTriggers
public List<OperableTrigger> acquireNextTriggers(final long noLaterThan, final int maxCount, final long timeWindow)
throws JobPersistenceException {
//默认maxCount为1,因此默认的时候是不会使用悲观锁。
// 那为何集群模式默认不使用锁呢?这里稍后解释用处。
String lockName;
if(isAcquireTriggersWithinLock() || maxCount > 1) {
lockName = LOCK_TRIGGER_ACCESS;
} else {
lockName = null;
}
return executeInNonManagedTXLock(lockName,
new TransactionCallback<List<OperableTrigger>>() {
public List<OperableTrigger> execute(Connection conn) throws JobPersistenceException {
return acquireNextTrigger(conn, noLaterThan, maxCount, timeWindow);
}
},
new TransactionValidator<List<OperableTrigger>>() {
public Boolean validate(Connection conn, List<OperableTrigger> result) throws JobPersistenceException {
try {
List<FiredTriggerRecord> acquired = getDelegate().selectInstancesFiredTriggerRecords(conn, getInstanceId());
Set<String> fireInstanceIds = new HashSet<String>();
for (FiredTriggerRecord ft : acquired) {
fireInstanceIds.add(ft.getFireInstanceId());
}
for (OperableTrigger tr : result) {
if (fireInstanceIds.contains(tr.getFireInstanceId())) {
return true;
}
}
return false;
} catch (SQLException e) {
throw new JobPersistenceException("error validating trigger acquisition", e);
}
}
});
}
3.1.2、 acquireNextTrigger获取触发器
protected List<OperableTrigger> acquireNextTrigger(Connection conn, long noLaterThan, int maxCount, long timeWindow){
// 中间省略部分代码
//1.
List<TriggerKey> keys = getDelegate().selectTriggerToAcquire(conn, noLaterThan + timeWindow, getMisfireTime(), maxCount);
}
// 中间省略部分代码
//2.
JobKey jobKey = nextTrigger.getJobKey();
JobDetail job;
try {
job = retrieveJob(conn, jobKey);
} catch (JobPersistenceException jpe) {
try {
getLog().error("Error retrieving job, setting trigger state to ERROR.", jpe);
getDelegate().updateTriggerState(conn, triggerKey, STATE_ERROR);
} catch (SQLException sqle) {
getLog().error("Unable to set trigger state to ERROR.", sqle);
}
continue;
}
//中间省略部分代码
//3.
int rowsUpdated = getDelegate().updateTriggerStateFromOtherState(conn, triggerKey, STATE_ACQUIRED, STATE_WAITING);
if (rowsUpdated <= 0) {
continue; // next trigger
}
//中间省略部分代码
//4.
getDelegate().insertFiredTrigger(conn, nextTrigger, STATE_ACQUIRED, null);
获取的过程很麻烦步骤如下。
- 获取
qrtz_triggers
表状态为WAITING
的触发器信息。order by NEXT_FIRE_TIME ASC ,PRIORITY DESC
按照这个规则进行排序。 - 验证查询回来的信息是否合法,查询
qrtz_job_details
表是否存在job信息。 - 如果triggers合法,则讲这条记录修改为
ACQUIRED
状态。这里的修改通过乐观锁的思想进行修改。通过StdJDBCConstants.UPDATE_TRIGGER_STATE_FROM_STATE
查看。 - 修改完成,则证明当前调度器可以对此作业进行调度。则插入
qrtz_fired_triggers
表中一条正在执行此作业的记录。
3.2、 qsRsrcs.getJobStore().triggersFired
插入qrtz_fired_triggers
的记录开始是为ACQUIRED
状态,这里修改为执行中EXECUTING
。
return executeInNonManagedTXLock(LOCK_TRIGGER_ACCESS,
new TransactionCallback<List<TriggerFiredResult>>() {
public List<TriggerFiredResult> execute(Connection conn) throws JobPersistenceException {
List<TriggerFiredResult> results = new ArrayList<TriggerFiredResult>();
TriggerFiredResult result;
for (OperableTrigger trigger : triggers) {
try {
TriggerFiredBundle bundle = triggerFired(conn, trigger);
result = new TriggerFiredResult(bundle);
} catch (JobPersistenceException jpe) {
result = new TriggerFiredResult(jpe);
} catch(RuntimeException re) {
result = new TriggerFiredResult(re);
}
results.add(result);
}
return results;
}
}
//省略部分代码
思路:
- 查询
qrtz_triggers
表是否是ACQUIRED
状态。 - 验证
qrtz_job_details
表的job是否合法。 - 修改
qrtz_fired_triggers
表为EXECUTING
状态。
这个执行的过程全程是加锁的。到这时修改成功的触发器即可在当前调度器进行调度。
3.3、JobRunShell.run任务调度
任务调度的源码在JobRunShell.run
方法中。
在此方法中,通过job.execute方法调度任务。jobExEx
记录各种异常信息,最后通过qs.notifyJobStoreJobComplete(trigger, jobDetail, instCode);
修改qrtz_triggers表状态,和删除qrtz_fired_triggers
表的信息。
附录
悲观锁与乐观锁
quartz框架中有多次通过qrtz_locks表的数据使用悲观锁来保证线程安全。数据如下,
public static final String SELECT_FOR_LOCK = "SELECT * FROM "
+ TABLE_PREFIX_SUBST + TABLE_LOCKS + " WHERE " + COL_SCHEDULER_NAME + " = " + SCHED_NAME_SUBST
+ " AND " + COL_LOCK_NAME + " = ? FOR UPDATE";
sql中通过for update
关键字来实现悲观锁。
悲观锁是很消耗性能的,但是这里却这么用了,肯定有必须要用的道理。思考至今得出,乐观锁适用于update语句之后做一些事物处理,主要通过update语句来过滤出只有一个线程能继续向下进行。悲观锁可以处理select语句之后的insert语句重复插入的问题,而且在几条数据状态频繁变更的时候,悲观锁也可以防止死锁的产生。
在第二步的时候也有插入操作,这个插入操作并没有在update语句之后,通过悲观锁可以防止重复插入。
第三步事务的代码中有众多状态变更,有可能出现死锁情况。如下图:
相信我,这两个都只是我的猜想,可能都是错的,如果您有更好的见解,请告诉我。