JDBC-Based JobStore指的是使用数据库持久化存储作业相关信息的JobStore,与之对应的是基于内存的RAMJobStore。
我们前面学习Quartz的Job、JobDetail、Trigger、作业调度线程以及作业执行线程的时候,大部分情况下是基于RAMJobStore进行分析的,所以对RAMJobStore已经有了了解,但是还不太了解JDBC-Based JobStore。
我们从以下几个角度分析JDBC-Based JobStore:
- 涉及到的表以及各表的作用
- 作业调度线程以及作业执行线程的基于JDBC-Based JobStore的主要工作过程
- JDBC-Based JobStore的事务管理及锁机制
- JDBC-Based JobStore的recovery及Misfire处理
以上4点放在一片文章中可能会太长,所以我们可能会分2篇文章进行分析,今天先分析第1/2两个问题。
JDBC-Based JobStore涉及到的表
JDBC-Based JobStore涉及到的表(省略前缀qrtz_):
- JOBDETAILS:作业信息
- TRIGGERS:Trigger信息
- SIMPLE_TRIGGERS:Simple Trigger信息
- CRON_TRIGGERS:Cron Trigger信息
- FIRED_TRIGGERS:被触发的Triggers
- SCHEDULER_STATE:调度服务的状态
- LOCKS:锁
Job及Trigger的注册
注册过程我们在前面的文章中已经说过了,RAMJobStore与JDBC-Based JobStore的区别主要是存储方式不同,一个存储在内存中,一个存储在数据库中。
Job注册后存储在JOBDETAILS表中,内容与存储在内存中基本一致,主要包括sched_name、name、group、description、job_class_name、job_data等。
Trigger注册后存储在TRIGGERS表中,内容与存储在内存中的也基本一致,主要包括sched_name、name、group、job_name、job_group、trigger_state、trigger_type、start_time、end_time、calendar_name、misfire_instr、job_data等。
trigger_state在初始化写入之后的状态为WAITING(等待被触发)。
trigger_type包括:
- SIMPLE:Simple Trigger
- CRON:Cron Trigger
- CAL_INT:Calendar Interval Trigger
- DAILY_I:Daily Time Interval Trigger
- BLOB:A general blob Trigger
不论是jobdetails还是trigger表都有一个sched_name字段,记录当前的任务调度器名称,sched_name从配置文件中取值(org.quartz.scheduler.instanceName)。
这个调度器名称sched_name其实就是运行任务调度服务的服务器标识,也就是当前正在运行quartz的服务的标识,集群环境下,不同的服务必须指定不同的sched_name,有关quartz集群的细节我们在其他文章做详细分析。
Trigger注册的时候还涉及到其他表:
如果当前Trigger处于Group Paused状态,Trigger同时写入paused_trigger_grps表。
Simple Trigger会写入Simple_triggers表,Cron Trigger会写入Cron_triggers表,负责记录各自的特殊属性;Simple_triggers记录repeat_count/repeat_interval/times_triggered等信息,Cron_triggers表记录cron表达式。
作业的调度
调度任务的执行逻辑我们在前面的文章中已经反复分析过:
- 从作业执行线程池获取availThreadCount,也就是当前可用的线程数
- 调用JobStore的acquireNextTriggers方法,获取特定短时间(idleWaitTime,默认30秒)内可能需要被触发的,数量不超过availThreadCount的触发器
- 调用JobStore的triggersFired方法对获取到的可能需要被触发的触发器进行二次加工,再次获取到最终的待触发的触发器结果集
- 循环处理最终的待处理触发器结果集中的每一个需要被触发的触发器
- 用JobRunShell包装该触发器做绑定的Job,送给线程池执行作业
JobStoreSupport#acquireNextTriggers
JobStoreSupport是JDBC-Based JobStore的抽象类,有两个实现类JobStoreCMT和JobStoreTX,两个实现类的主要作用是管理事务,大部分的业务逻辑其实都是在JobStoreSupport中实现的,包括acquireNextTriggers和triggersFired方法。
acquireNextTriggers方法首先从Triggers表中获取符合条件的触发器:nextFireTime在30秒内(有参数idleWaitTime设定)的、状态为WAITING的、一定数量的(参数设置的一次处理的触发器个数、或者可用的执行线程数)的触发器。
针对获取到的每一个Trigger:
- 从JobDetails表中获取其绑定的Job,判断Job的ConcurrentExectionDisallowed属性,做并发控制(逻辑与RAMJobStore的相关逻辑一样)
- Triggers表中当前trigger的状态从WAITING修改为ACQUIRED
- 当前Trigger写入到fired_triggers表中,状态为ACQUIRED
JobStoreSupport#triggersFired
再次从triggers表中获取到当前trigger,验证其状态是否为ACQUIRED,状态不正确的不做处理。
从job_details表中获取到当前trigger绑定的作业。
更新fired_triggers表中当前trigger的状态为EXECUTING。
调用trigger的triggered方法获取当前trigger的下一次触发时间。
更新triggers表中当前trigger的状态(WAITING)、下次触发时间等数据。
作业执行#JobRunShell
通过以上步骤,作业调度线程就获取到了当前需要执行的trigger,之后就需要执行最后一步:
用JobRunShell包装该触发器,送给线程池执行该触发器关联的作业
我们需要对这个JobRunShell做一个简单了解。
JobRunShell instances are responsible for providing the 'safe' environment for Job s to run in, and for performing all of the work of executing the Job, catching ANY thrown exceptions, updating the Trigger with the Job's completion code, etc.
A JobRunShell instance is created by a JobRunShellFactory on behalf of the QuartzSchedulerThread which then runs the shell in a thread from the configured ThreadPool when the scheduler determines that a Job has been triggered.
JavaDoc说的非常清楚,JobRunShell其实才是最终负责Job运行的,SimpleThreadPool只是提供了运行Job的线程,有任务交进来之后(交进来的其实是持有job对象的JobRunShell对象)SimpleThreadPool负责分配一个执行线程、之后用该线程运行该任务。
我们在前面分析SimpleThreadPool的文章中已经说过,线程池的执行线程WorkThread有一个Runable接口的对象,任务触发后Job会封装为这个Runable对象、然后交给WorkThread用一个新的线程执行这个Runable对象。
这个Runable对象就是JobRunShell,JobRunShell实现了Runable接口。
所以我们就从JobRunShell的run方法入手。
JobRunShell#run
JobRunShell在初始化的时候封装了一个JobExecutionContextImpl对象jec,其中包含了Job、Trigger、Scheduler等相关对象。
从jec获取到job,调用job的execute方法,这里其实就调用到了我们应用层中实现了Job接口对象的execute方法,其实是我们的业务逻辑被调用执行了。。
作业调用执行完成之后,回调QuartzScheduler的notifyJobStoreJobComplete方法,通知调度器当前trigger已经完成执行。
QuartzScheduler的notifyJobStoreJobComplete方法调用JobStore的triggeredJobComplete方法。
JobStoreSuppor#triggeredJobComplete
根据trigger执行情况更新triggers表中当前trigger的状态,如果trigger的nextFireTime不为空的话更新为WAITING,等待下次被触发,如果为空的话则更新为COMPLETED,任务执行完成。除此之外还有其他的挂起、错误等状态。
当前trigger从fired_triggers表中删除。
Trigger State
Trigger的状态其实和JobStore无关,也就是说不管是用基于内存的JobStore,还是基于数据库的JobStore,对于Trigger状态的管理逻辑都是一样的。
Trigger的状态包括:
- WAITING:初始化状态,等待被调度
- ACQUIRED:已经被调度器获取,等待被触发
- EXECUTING:正在执行
- COMPLETE:执行完成,不再被调度
- BLOCKED:阻塞
- ERROR:错误
- PAUSED:挂起/暂停
- PAUSED_BLOCKED:挂起-阻塞
- DELETED:被删除
Trigger注册的时候,如果在挂起列表(qrtz_paused_trigger_grps)中的话,状态为PAUSED,否则状态为WAITING。
Trigger被调度器调度之后(状态为WAITING、nextFiredTime在30秒之内)状态为ACQUIRED,被作业执行线程调起执行的时候状态变更为EXECUTING,作业执行完成后,如果Trigger不再需要被执行(执行次数或执行时间达到了设置要求)则状态为COMPLETE。
设置为ConcurrentExectionDisallowed的作业被调度器执行后,当前作业绑定的其他Trigger的状态:
- WAITING -> BLOCKED
- ACQUIRED -> BLOCKED
- PAUSED -> PAUSED_BLOCKED
作业被调度器调度执行后,当前Trigger的状态:
- 如果当前Trigger被触发后,nextFiredTime不为空的话,状态设置为WAITING,等待下次被触发
- 否则,设置为COMPLETE,触发器完成使命
JobRunShell完成作业执行后通过调用notifyJobStoreJobComplete方法通知JobStore触发器完成本次作业调度后,如果当前Trigger绑定的作业设置为ConcurrentExectionDisallowed,则设置当前作业绑定其他Trigger的状态(与作业执行时做相反操作以回复这些触发器到正常状态):
- BLOCKED -> WAITING
- PAUSED_BLOCKED -> PAUSED
我们可以通过调用scheduler的pauseTrigger方法暂停/挂起当前任务,调用后触发器的状态:
- WAITING -> PAUSED
- ACQUIRED -> PAUSED
- BLOCKED -> PAUSED_BLOCKED
被暂停的任务被重新调起后,状态恢复。
小结
到这儿,开篇设置的本篇文章的任务就完成了,后续的两个问题下一篇文章继续。
Thanks a lot!