Trigger:Quartz的触发器,任务需要和Trigger绑定之后并最终被Trigger的执行规则触发之后才能被调用执行。
JobStore是Quartz用来保存作业执行现场的组件,我们知道JobDetail和Trigger是要注册并绑定到任务调度器中之后,Job才能最终被执行。这个注册动作最终实际上是把JobDetail和Trigger送到JobStore保存起来,JobStore是JobDetail和Trigger的容器。
SimpleTrigger和CronTrigger
Quartz实现了两个比较重要也比较常用的触发器:SimpleTrigger和CronTrigger。
SimpleTrigger是简单触发器,可以实现比较简单的触发规则,比如什么时间开始执行、每隔多久执行一次、共执行多少次、到什么时间结束任务等等。
CronTrigger是支持cron表达式的触发器,由于可以支持cron表达式,所以CronTrigger可以支持非常复杂的触发规则,比如每天几点、每星期几几点、每月几号几点等等。
SimpleTrigger和CronTrigger都可以通过Calendar支持规避某些特定时间点触发任务,比如周六日不触发、节假日不触发等等。
Trigger实现的底层原理
我们从以下两个角度了解Trigger实现的底层原理:
- Trigger的注册(绑定)
- RAMJobStore对Trigger注册的实现
- Trigger的触发过程
- RAMJobStore对Trigger触发过程的实现
Trigger的注册
上一篇文章已经分析过Trigger和JobDetail的绑定过程,最终是通过QuartzScheduler的scheduleJob方法完成绑定,看一下源码:
public Date scheduleJob(JobDetail jobDetail,
Trigger trigger) throws SchedulerException {
validateState();
if (jobDetail == null) {
throw new SchedulerException("JobDetail cannot be null");
}
//省略代码
resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
notifySchedulerListenersJobAdded(jobDetail);
notifySchedulerThread(trigger.getNextFireTime().getTime());
notifySchedulerListenersSchduled(trigger);
//省略代码
最终是调用了JobStore的storeJobAndTrigger方法完成注册和绑定。
Quartz可以支持只在内存存储的JobStore:RAMJobStore,也可以支持持久化到数据库的JobStore:JobStoreSupport。今天先研究RAMJobStore,其他类型的JobStore放在后面学习。
RAMJobStore#JobDetail注册
Trigger与JobDetail是绑定注册到RAMJobStore中的。
JobDetail是将clone后的实现对象包装后(JobWrapper)注册到RAMJobStore中的,分别存入到两个容器中:jobsByKey和jobsByGroup容器中。而且,相同的JobDetail是不允许重复注册的,这个我们在上一篇文章中说过,JobDetail以JobKey作为键值来唯一识别。JobDetail注册的时候如果容器jobsByKey中已经存在另外一个JobKey相同的JobDetail,那么,要么替换掉、要么抛异常,取决于注册时传入的是否允许替换参数。
Trigger和JobDetail的绑定
Tigger注册之前需要首先和JobDetail进行绑定,两者的绑定是通过为Trigger设定JobKey实现的,Trigger可以通过两种方法实现与JobDetail的绑定:
- Triiger创建的时候:通过forJob指定该Trigger绑定的JobDetail
- 通过任务调度器的scheduleJob方法(需要JobDetail和Trigger参数)绑定
RAMJobStore#Trigger注册
与JobDetail类似,Trigger也是将clone后的包装对象(TriggerWrapper)注册到RAMJobStore中的。
与JobDetail类似,Trigger包含一个以TriggerKey作为Trigger的唯一键值。不允许重复键值的Trigger注册到JobStore中。
Trigger会注册到RAMJobStore的如下容器中:
- triggersByKey:以TriggerKey为键/值为Trigger的HashMap。
- triggersByJob:以Trigger的JobKey为键的HashMap,因为同一个JobDetail可以绑定到不同的Trigger上,所以triggersByJob的值为该JobDetail对应的Trigger组成的List
- triggersByGroup:键为Trigger的group/值为该group下所有Triggers组成的HashMap的HashMap
- timeTriggers:允许被触发的Trigger组成的TreeSet
- blockedJobs:被阻塞的作业组成的HashSet
需要简单说明一下timeTriggers和blockedJobs,触发器注册到RAMJobStore的时候,如果该触发器绑定的Job不在blockedJobs中的话,则直接加入到timeTriggers等待被触发,否则如果在blockedJobs中的话就不会加入到timeTriggers中所以暂时也就不会被触发,触发器的状态也会被设置为阻塞状态。
如果Job实现类加了注解@DisallowConcurrentExecution,则该实现类以某一jobKey绑定到RAMJobStore中的Trigger如果有多个的话,则不允许这些Triggers并发。
为了实现以上不允许并发的控制,当作业被某一触发器触发后,Quartz会将该作业的所有其他触发器移出timeTriggers,并将该作业放入到blockedJobs中。
没有以上控制要求的触发器,在注册后都放入到timeTriggers中等待被触发。
Trigger触发作业过程
Trigger毫无疑问应该是在作业调度线程QuartzSchedulerThread的run方法中被触发,从源码可以发现其触发逻辑为:
- 从作业执行线程池获取availThreadCount,也就是当前可用的线程数
- 调用JobStore的acquireNextTriggers方法,获取特定短时间(idleWaitTime)内可能需要被触发的,数量不超过availThreadCount的触发器
- 调用JobStore的triggersFired方法对获取到的可能需要被触发的触发器进行二次加工,再次获取到最终的待触发器结果集
- 循环处理最终的待处理触发器结果集中的每一个需要被触发的触发器
- 用JobRunShell包装该触发器,送给线程池执行该触发器关联的作业
下面我们看一下RAMJobStore对以上两个方法的实现。
RAMJobStore#acquireNextTriggers
获取在idleWaitTime(默认30秒)内需要被触发的触发器。
我们知道Trigger有一个重要属性nextFireTime,用来记录该触发器的下次触发时间,触发器每次被触发、作业被执行后都会重新按照触发器的规则计算下次触发时间。
nextFireTime==null则表明该触发器不再会被调用,Quartz会将该触发器移出排队队列。
RAMJobStore的acquireNextTriggers方法从timeTriggers中获取每一个Trigger进行判断,如果符合规则(在idleWaitTime应该被触发)则加入到待触发结果集中返回。
如果该触发器的下次触发时间在idleWaitTime之后,则该触发器本次不需要被处理。
否则,该触发器应该被加入到待触发结果集中。
但是仍然需要检查当前触发器绑定的作业是否设置了@DisallowConcurrentExecution,如果设置为不允许并发的话,该作业关联的触发器仅有一个会被加入到待触发结果集中。
否则如果没有设置@DisallowConcurrentExecution的话,直接将当前触发器加入到待触发结果集中。
加入到待触发结果集中的触发器同时会从timeTriggers中移除。
最后将获取到的待触发结果集返回。
RAMJobStore#triggersFired
逐一检查待触发结果集中的触发器。
为确保安全,再次从timeTriggers移除当前触发器。
调用Trigger的triggered方法,该方法主要作用是重新计算触发器的nextFireTime。
用当前触发器、其绑定的作业创建TriggerFiredBundle。
如果触发器绑定的作业设置了@DisallowConcurrentExecution,则将该作业的所有触发器移出timeTriggers并将该作业加入blockedJobs。
否则,如果重新计算之后的nextFireTime不为空的话,说明当前触发器后续仍然会有触发需求(比如每天下午2点触发的话,则该触发器的nextFireTime应该为第二天的下午2点),将该触发器再加入timeTriggers中,等待下次触发。
以上逻辑是重复执行的触发器之所以能被反复执行的原因。
将组装好的TriggerFiredBundle加入到最终的待触发结果集中返回。
最终待处理触发器结果集返回给QuartzSchedulerThread的run方法后将被调用执行。
至此,作业从配置到执行的逻辑我们分析完毕。
触发器在待处理队列中的顺序
timeTriggers是待处理触发器队列,其数据结构是TreeSet,我们知道TreeSet是有顺序的,所以,放进去的触发器应该也是有顺序的。
这个顺序,我们其实不难想到,是按照下次执行时间、以及优先级排序的。Quartz实现有序队列的逻辑是在TriggerWrapperComparator中,TriggerWrapperComparator作为timeTriggers的比较器、在timeTriggers初始化的时候被创建。
比较器TriggerWrapperComparator的compare方法:
最终调用了TriggerTimeComparator的compare方法,从类名称上我们也可以发现他应该是按照时间排序的,方法源码非常简单,就是首先按照nextFireTime、再按照priority进行排序的。
以上!