Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。该项目于 2009 年被 Terracotta 收购,目前是 Terracotta 旗下的一个项目。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-quartzartifactId>
dependency>
org.quartz 包下,有一个默认的配置文件,quartz.properties。当我们没有定义一个同名的配置文件的时候,就会使用默认配置文件里面的配置。
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
实现唯一的方法 execute(),方法中的代码就是任务执行的内容。此处仅输出字符串。
public class MyJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("my job...");
}
}
在测试类或者 main() 方法中,把 Job 进一步包装成 JobDetail。
必须要指定 JobName 和 groupName,两个合起来是唯一标识符。
可以携带 Key、Value 的数据(JobDataMap),用于扩展属性,在运行的时候可以从 context 获取到
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1", "group1")
.usingJobData("age","21")
.usingJobData("name","zs")
.build();
在测试类或者 main() 方法中,基于 SimpleTrigger 定义了一个每 2 秒钟运行一次、不断重复的 Trigger:
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(2)
.repeatForever())
.build();
在测试类 main()方法中,通过 Factory 获取调度器的实例,把 JobDetail 和 Trigger绑定,注册到容器中。
Scheduler 先启动后启动无所谓,只要有 Trigger 到达触发条件,就会执行任务。
SchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。实现Job接口的任务,默认是无状态的,若要将Job设置成有状态的,在quartz中是给实现的Job添加@DisallowConcurrentExecution注解
无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存
用来描述Job实现类,其它相关的静态信息,如Job名字、关联监听器等信息。
触发器,用于定义任务调度的时间规则,Trigger,使用 TriggerBuilder 来构建。
JobDetail 跟 Trigger 是 1:N 的关系。
Trigger 接口在 Quartz 有 4 个继承的子接口:
子接口 | 描述 | 特点 |
---|---|---|
SimpleTrigger | 简单触发器 | 固定时刻或时间间隔,毫秒 |
CalendarIntervalTrigger | 基于日历的触发器 | 比简单触发器更多时间单位,支持非固定时间的触发,例如一年可能 365/366,一个月可能 28/29/30/31 |
DailyTimeIntervalTrigger | 基于日期的触发器 | 每天的某个时间段 |
CronTrigger | 基于 Cron 表达式的触发器 |
类图
CronTirgger 可以定义基于 Cron 表达式的调度规则,是最常用的触发器类型。
上面我们定义的都是在什么时间执行,但是我们有一些在什么时间不执行的需求,比如:理财周末和法定假日购买不计息;证券公司周末和法定假日休市。
如果要在触发器的基础上,排除一些时间区间不执行任务,就要用到Quartz的Calendar类(注意不是JDK的Calendar)。可以按年、月、周、日、特定日期、Cron表达式排除。
调用scheduler.addCalendar()可定义排除规则
代码如下:
// 定义日历
AnnualCalendar holidays = new AnnualCalendar();
// 排除中秋节
Calendar midAutumn = new GregorianCalendar(2019, 9, 13);
holidays.setDayExcluded(midAutumn, true);
// 排除圣诞节
Calendar christmas = new GregorianCalendar(2019, 12, 25);
holidays.setDayExcluded(christmas, true);
// 调度器添加日历
scheduler.addCalendar("holidays", holidays, false, false);
Calendar名称 | 用法 |
---|---|
BaseCalendar | 为高级的 Calendar 实现了基本的功能,实现了 org.quartz.Calendar 接口 |
AnnualCalendar | 排除年中一天或多天 |
CronCalendar | 日历的这种实现排除了由给定的CronExpression表达的时间集合。 例如,您可以使用此日历使用表达式“* * 0-7,18-23?* *”每天排除所有营业时间(上午8点至下午5点)。 如果CronTrigger具有给定的cron表达式并且与具有相同表达式的CronCalendar相关联,则日历将排除触发器包含的所有时间,并且它们将彼此抵消。 |
DailyCalendar | 您可以使用此日历来排除营业时间(上午8点 - 5点)每天。 每个DailyCalendar仅允许指定单个时间范围,并且该时间范围可能不会跨越每日边界(即,您不能指定从上午8点至凌晨5点的时间范围)。 如果属性invertTimeRange为false(默认),则时间范围定义触发器不允许触发的时间范围。 如果invertTimeRange为true,则时间范围被反转 - 也就是排除在定义的时间范围之外的所有时间。 |
HolidayCalendar | 特别的用于从 Trigger 中排除节假日 |
MonthlyCalendar | 排除月份中的指定数天,例如,可用于排除每月的最后一天 |
WeeklyCalendar | 排除星期中的任意周几,例如,可用于排除周末,默认周六和周日 |
调度器,是Quartz的指挥官,由StdSchedulerFactory产生。它是单例的。
并且是Quartz中最重要的API,默认是实现类是StdScheduler,里面包含了一个QuartzScheduler。QuartzScheduler里面又包含了一个QuartzSchedulerThread。
Scheduler中的方法主要分为三大类:
1)操作调度器本身,例如调度器的启动start()、调度器的关闭shutdown()。
2)操作Trigger,例如暂停触发器pauseTriggers()、恢复触发器resumeTrigger()。
3)操作Job,例如执行job scheduleJob()、移除触发器unscheduleJob()、更新job的参数rescheduleJob()
我们有这么一种需求,在每个任务运行结束之后发送通知给运维管理员。那是不是要在每个任务的最后添加一行代码呢?这种方式对原来的代码造成了入侵,不利于维护。如果代码不是写在任务代码的最后一行,怎么知道任务执行完了呢?或者说,怎么监测到任务的生命周期呢?
观察者模式:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并自动更新。
Quartz中提供了三种Listener,监听Scheduler的,监听Trigger的,监听Job的。
只需要创建类实现相应的接口,并在Scheduler上注册Listener,便可实现对核心对象的监听。
Jobstore用来存储任务和触发器相关的信息,例如所有任务的名称、数量、状态等等。Quartz中有两种存储任务的方式,一种在在内存,一种是在数据库。
Quartz默认的JobStore是RAMJobstore,也就是把任务和触发器信息运行的信息存储在内存中
如果程序崩溃或重启,所有存储在内存中的数据都会丢失。所以我们需要把这些数据持久化到磁盘
JDBCJobStore可以通过JDBC接口,将任务运行数据保存在数据库中。
JDBC的实现方式有两种,JobStoreSupport类的两个子类:
JobStoreTX:在独立的程序中使用,自己管理事务,不参与外部事务。
JobStoreCMT:(Container Managed Transactions (CMT),如果需要容器管理事务时,使用它。
使用JDBCJobSotre时,需要配置数据库信息:
org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 使用quartz.properties,不使用默认配置
org.quartz.jobStore.useProperties:true
#数据库中quartz表的表名前缀
org.quartz.jobStore.tablePrefix:QRTZ_
org.quartz.jobStore.dataSource:myDS
#配置数据源
org.quartz.dataSource.myDS.driver:com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL:jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=utf8
org.quartz.dataSource.myDS.user:root
org.quartz.dataSource.myDS.password:123456
org.quartz.dataSource.myDS.validationQuery=select 0 from dual
问题来了?需要建什么表?表里面有什么字段?字段类型和长度是什么?
在官网的Downloads链接中,提供了11张表的建表语句
2.3的版本在包的这个路径下:
org.quartz.impl.jdbcjobstore
表名与作用:
表名 | 作用 |
---|---|
QRTZ_BLOB_TRIGGERS | Trigger作为Blob类型存储 |
QRTZ_CALENDARS | 存储Quartz的Calendar信息 |
QRTZ_CRON_TRIGGERS | 存储CronTrigger,包括Cron表达式和时区信息 |
QRTZ_FIRED_TRIGGERS | 存储与已触发的Trigger相关的状态信息,以及相关Job的执行信息 |
QRTZ_JOB_DETAILS | 存储每一个已配置的Job的详细信息 |
QRTZ_LOCKS | 存储程序的悲观锁的信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组的信息 |
QRTZ_SCHEDULER_STATE | 存储少量的有关Scheduler的状态信息,和别的Scheduler实例 |
QRTZ_SIMPLE_TRIGGERS | 存储SimpleTrigger的信息,包括重复次数、间隔、以及已触的次数 |
QRTZ_SIMPROP_TRIGGERS | 存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种类型的触发器 |
QRTZ_TRIGGERS | 存储已配置的Trigger的信息 |
Spring在spring-context-support.jar中直接提供了对Quartz的支持。
可以在配置文件中把JobDetail、Trigger、Scheduler定义成Bean。
<bean name="myJob1" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="name" value="my_job_1"/>
<property name="group" value="my_group"/>
<property name="jobClass" value="com.ly.quartz.MyJob1"/>
<property name="durability" value="true"/>
bean>
<bean name="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean"> <property name="name" value="my_trigger_1"/> <property name="group" value="my_group"/> <property name="jobDetail" ref="myJob1"/> <property name="startDelay" value="1000"/> <property name="repeatInterval" value="5000"/> <property name="repeatCount" value="2"/>
bean>
<bean name="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="simpleTrigger"/>
<ref bean="cronTrigger"/>
list>
property>
bean>
既然可以在配置文件配置,当然也可以用@Bean注解配置。在配置类上加上@Configuration让Spring读取到。
public class QuartzConfig {
@Bean
public JobDetail printTimeJobDetail(){
return JobBuilder.newJob(MyJob1.class)
.withIdentity("aJob")
.usingJobData("name", "ly")
.storeDurably()
.build();
}
@Bean
public Trigger printTimeJobTrigger() {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
return TriggerBuilder.newTrigger()
.forJob(printTimeJobDetail())
.withIdentity("quartzTaskService")
.withSchedule(cronScheduleBuilder)
.build();
}
}
传统的Spring方式集成,由于任务信息全部配置在xml文件中,如果需要操作任务或者修改任务运行频率,只能重新编译、打包、部署、重启,如果有紧急问题需要处理,会浪费很多的时间。
对于这种频繁变更并且需要实时生效的配置信息,我们可以放到数据库里,提供一个界面,实现对数据表的操作
问题1:参考JobDetail的属性创建表
CREATE TABLE `sys_job` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`job_name` VARCHAR ( 512 ) NOT NULL COMMENT '任务名称',
`job_group` VARCHAR ( 512 ) NOT NULL COMMENT '任务组名',
`job_cron` VARCHAR ( 512 ) NOT NULL COMMENT '时间表达式',
`job_class_path` VARCHAR ( 1024 ) NOT NULL COMMENT '类路径,全类型',
`job_data_map` VARCHAR ( 1024 ) DEFAULT NULL COMMENT '传递map参数',
`job_status` INT ( 2 ) NOT NULL COMMENT '状态:1启用 0停用',
`job_describe` VARCHAR ( 1024 ) DEFAULT NULL COMMENT '任务功能描述',
PRIMARY KEY ( `id` )
) ENGINE = INNODB AUTO_INCREMENT = 25 DEFAULT CHARSET = utf8;
但是在修改了表的数据之后,怎么让调度器知道呢?
调度器的接口:Scheduler
1、 新增一个任务
scheduler.scheduleJob(jobDetail, trigger)
2、删除一个任务
scheduler.deleteJob(JobKey.jobKey(jobName, jobGroupName));
3、启动一个任务
scheduler.resumeJob(JobKey.jobKey(jobName, jobGroupName));
4、停止一个任务
scheduler.pauseJob(JobKey.jobKey(jobName, jobGroupName));
5、修改任务的信息(包括调度规律)
scheduler.rescheduleJob(triggerKey, trigger);
容器启动与Service注入
因为任务没有定义在ApplicationContext.xml中,而是放到了数据库中,Spring Boot启动时,怎么读取任务信息?
创建一个类,实现CommandLineRunner接口,实现run方法。
Job的实现类使用bean
Job的实现类如何引入service及调用service方法
例如在TestTask3中,需要注入ISysJobService,查询数据库发送邮件,如果没有任何配置,注入会报空指针异常。
原因:
因为定时任务Job对象的实例化过程是在Quartz中进行的,而Service Bean是由Spring容器管理的,Quartz察觉不到Service Bean的存在,所以无法将Service Bean装配到Job对象中。
分析:
Quartz集成到Spring中,用到SchedulerFactoryBean,其实现了InitializingBean方法,在唯一的方法afterPropertiesSet()在Bean的属性初始化后调用。
调度器用AdaptableJobFactory对Job对象进行实例化。所以,如果我们可以把这个JobFactory指定为我们自定义的工厂的话,就可以在Job实例化完成之后,把Job纳入到Spring容器中管理。
解决这个问题的步骤:
1、定义一个AdaptableJobFactory,实现JobFactory接口,实现接口定义的newJob方法,在这里面返回Job实例
2、定义一个MyJobFactory,继承AdaptableJobFactory。
使用Spring的AutowireCapableBeanFactory,把Job实例注入到容器中。
@Component
public class MyJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object jobInstance = super.createJobInstance(bundle);
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
3、指定Scheduler的JobFactory为自定义的JobFactory。
scheduler.setJobFactory(myJobFactory);
1、防止单点故障,减少对业务的影响
2、减少节点的压力,例如在10点要触发1000个任务,如果有10个节点,则每个节点之需要执行100个任务
1、任务重跑,因为节点部署的内容是一样的,到10点的时候,每个节点都会执行相同的操作,引起数据混乱
2、任务漏跑,假如任务是平均分配的,本来应该在某个节点上执行的任务,因为节点故障,一直没有得到执行。
3、水平集群需要注意时间同步问题
所以必须要有一种共享数据或者通信的机制。在分布式系统的不同节点中,我们可以采用什么样的方式,实现数据共享?
例如:ZK、Redis、DB。
在Quartz中,提供了一种简单的方式,基于数据库共享任务执行信息。也就是说,一个节点执行任务的时候,会操作数据库,其他的节点查询数据库,便可以感知
1、创建表
如上2.6.5.2的JDBCJobSotre
1、修改quartz.properties配置
四个配置:集群实例ID、集群开关、数据库持久化、数据源信息
问题:
1、Job没有继承Thread和实现Runnable,是怎么被调用的?通过反射还是什么?
2、任务是什么时候被调度的?是谁在监视任务还是监视Trigger?
3、任务是怎么被调用的?谁执行了任务?
4、任务本身有状态吗?还是触发器有状态?
看源码的入口
Scheduler scheduler = factory.getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
public Scheduler getScheduler() throws SchedulerException {
if (cfg == null) {
// 读取quartz.properties配置文件
initialize();
}
// 这个类是一个HashMap,用来基于调度器的名称保证调度器的唯一性
SchedulerRepository schedRep = SchedulerRepository.getInstance();
Scheduler sched = schedRep.lookup(getSchedulerName());
// 如果调度器已经存在了
if (sched != null) {
// 调度器关闭了,移除
if (sched.isShutdown()) {
schedRep.remove(getSchedulerName());
} else {
// 返回调度器
return sched;
}
}
// 调度器不存在,初始化
sched = instantiate();
return sched;
}
instantiate()方法中做了初始化的所有工作:
// 存储任务信息的JobStore
JobStore js = null;
// 创建线程池,默认是SimpleThreadPool
ThreadPool tp = null;
// 创建调度器
QuartzScheduler qs = null;
// 连接数据库的连接管理器
DBConnectionManager dbMgr = null;
// 自动生成ID
// 创建线程执行器,默认为DefaultThreadExecutor
ThreadExecutor threadExecutor;
842行和851行,创建了一个线程池,默认是配置文件中指定的SimpleThreadPool。
String tpClass = cfg.getStringProperty(PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName());
tp = (ThreadPool) loadHelper.loadClass(tpClass).newInstance();
SimpleThreadPool里面维护了三个list,分别存放所有的工作线程、空闲的工作线程和忙碌的工作线程。我们可以把SimpleThreadPool理解为包工头。
private List<WorkerThread> workers;
private LinkedList<WorkerThread> availWorkers = new LinkedList<WorkerThread>();
private LinkedList<WorkerThread> busyWorkers = new LinkedList<WorkerThread>();
tp的runInThread()方法是线程池运行线程的接口方法。参数Runnable是执行的任务内容。
取出WorkerThread去执行参数里面的runnable(JobRunShell)。
WorkerThread wt = (WorkerThread)availWorkers.removeFirst();
busyWorkers.add(wt);
wt.run(runnable);
WorkerThread是SimpleThreadPool的内部类,用来执行任务。我们把WorkerThread理解为工人。在WorkerThread的run方法中,执行传入的参数runnable任务:
runnable.run();
1333行,创建了调度器QuartzScheduler:
qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);
在 QuartzScheduler的构造函数中,创建了QuartzSchedulerThread,我们把它理解为项目经理,它会调用包工头的工人资源,给他们安排任务。
并且创建了线程执行器schedThreadExecutor,执行了这个QuartzSchedulerThread,也就是调用了它的run方法。
// 创建一个线程,resouces里面有线程名称
this.schedThread = new QuartzSchedulerThread(this, resources);
// 线程执行器
ThreadExecutor schedThreadExecutor = resources.getThreadExecutor();
//执行这个线程,也就是调用了线程的run方法
schedThreadExecutor.execute(this.schedThread);
点开 QuartzSchedulerThread 类,找到run方法,这个是Quartz任务调度的核心方法:
public void run() {
int acquiresFailed = 0;
// 检查scheuler是否为停止状态
while (!halted.get()) {
try {
// check if we're supposed to pause...
synchronized (sigLock) {
// 检查是否为暂停状态
while (paused && !halted.get()) {
try {
// wait until togglePause(false) is called...
// 暂停的话会尝试去获得信号锁,并wait一会
sigLock.wait(1000L);
} catch (InterruptedException ignore) {
}
// reset failure counter when paused, so that we don't
// wait again after unpausing
acquiresFailed = 0;
}
if (halted.get()) {
break;
}
}
// wait a bit, if reading from job store is consistently
// failing (e.g. DB is down or restarting)..
// 从JobStore获取Job持续失败,sleep一下
if (acquiresFailed > 1) {
try {
long delay = computeDelayForRepeatedErrors(qsRsrcs.getJobStore(), acquiresFailed);
Thread.sleep(delay);
} catch (Exception ignore) {
}
}
// 从线程池获取可用的线程
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...
List<OperableTrigger> triggers;
long now = System.currentTimeMillis();
clearSignaledSchedulingChange();
try {
// 获取需要下次执行的triggers
// idleWaitTime: 默认30s
// availThreadCount:获取可用(空闲)的工作线程数量,总会大于1,因为该方法会一直阻塞,直到有工作线程空闲下来。
// maxBatchSize:一次拉取trigger的最大数量,默认是1
// batchTimeWindow:时间窗口调节参数,默认是0
// misfireThreshold: 超过这个时间还未触发的trigger,被认为发生了misfire,默认60s
// 调度线程一次会拉取NEXT_FIRETIME小于(now + idleWaitTime +batchTimeWindow),大于(now - misfireThreshold)的,min(availThreadCount,maxBatchSize)个triggers,默认情况下,会拉取未来30s、过去60s之间还未fire的1个trigger
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
// 省略…………
// set triggers to 'executing'
List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();
boolean goAhead = true;
synchronized(sigLock) {
goAhead = !halted.get();
}
if(goAhead) {
try {
// 触发Trigger,把ACQUIRED状态改成EXECUTING
// 如果这个trigger的NEXTFIRETIME为空,也就是未来不再触发,就将其状态改为COMPLETE
// 如果trigger不允许并发执行(即Job的实现类标注了@DisallowConcurrentExecution),则将状态变为BLOCKED,否则就将状态改为WAITING
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
// 省略…………
continue;
}
}
// 循环处理Trigger
for (int i = 0; i < bndles.size(); i++) {
TriggerFiredResult result = bndles.get(i);
TriggerFiredBundle bndle = result.getTriggerFiredBundle();
Exception exception = result.getException();
// 省略…………
JobRunShell shell = null;
try {
// 根据trigger信息实例化JobRunShell(implements Runnable),同时依据JOB_CLASS_NAME实例化Job,随后我们将JobRunShell实例丢入工作线。
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;
}
// 执行JobRunShell的run方法
if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
// 省略…………
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.
JobRunShell用来为Job提供安全的运行环境的,执行Job中所有的作业,捕获运行中的异常,在任务执行完毕的时候更新Trigger状态,等等。
JobRunShell实例是用JobRunShellFactory为QuartzSchedulerThread创建的,在调度器决定一个Job被触发的时候,它从线程池中取出一个线程来执行任务。
**SimpleThreadPool:**包工头,管理所有WorkerThread
**WorkerThread:**工人,把Job包装成JobRunShell,执行
**QuartSchedulerThread:**项目经理,获取即将触发的Trigger,从包工头出拿到worker,执行Trigger绑定的任务
// 存储JobDetail和Trigger
resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
// 通知相关的Listener
notifySchedulerListenersJobAdded(jobDetail);
notifySchedulerThread(trigger.getNextFireTime().getTime());
notifySchedulerListenersSchduled(trigger);
// 通知监听器
notifySchedulerListenersStarting();
if (initialStart == null) {
initialStart = new Date();
this.resources.getJobStore().schedulerStarted();
startPlugins();
} else {
resources.getJobStore().schedulerResumed();
}
// 通知QuartzSchedulerThread不再等待,开始干活
schedThread.togglePause(false);
// 通知监听器
notifySchedulerListenersStarted();
getScheduler方法创建线程池ThreadPool,创建调度器QuartzScheduler,创建调度线程QuartzSchedulerThread,调度线程初始处于暂停状态。
scheduleJob将任务添加到JobStore中。
scheduler.start()方法激活调度器,QuartzSchedulerThread从timeTrriger取出待触发的任务,并包装成TriggerFiredBundle,然后由 JobRunShellFactory 创建TriggerFiredBundle的执行线程JobRunShell, 调度执行通过线程池SimpleThreadPool去执行JobRunShell, 而JobRunShell执行的就是任务类的execute方法:job.execute(JobExecutionContext context)。
QuartzSchedulerThread第287行,获取下一个即将触发的Trigger
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
调用 JobStoreSupport的acquireNextTriggers()方法,2793行
调用JobStoreSupport.executeInNonManagedTXLock()方法
return executeInNonManagedTXLock(lockName,
尝试获取锁,3857行:
transOwner = getLockHandler().obtainLock(conn, lockName);
下面有回滚和释放锁的语句,即使发生异常,锁同样能释放。
调用DBSemaphore的obtainLock()方法,103行
public boolean obtainLock(Connection conn, String lockName)
throws LockException {
if (!isLockOwner(lockName)) {
executeSQL(conn, lockName, expandedSQL, expandedInsertSQL);
调用StdRowLockSemaphore的executeSQL()方法,100行。
最终用JDBC执行SQL,语句内容是expandedSQL和expandedInsertSQL。
ps = conn.prepareStatement(expandedSQL);
问题:expandedSQL和expandedInsertSQL是一条什么SQL语句?
在StdRowLockSemaphore的构造函数中,把定义的两条SQL传进去:
public StdRowLockSemaphore() {
super(DEFAULT_TABLE_PREFIX, null, SELECT_FOR_LOCK, INSERT_LOCK);
}
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";
public static final String INSERT_LOCK = "INSERT INTO "
+ TABLE_PREFIX_SUBST + TABLE_LOCKS + "(" + COL_SCHEDULER_NAME + ", " + COL_LOCK_NAME + ") VALUES ("
+ SCHED_NAME_SUBST + ", ?)";
它调用了父类DBSemaphore的构造函数:
public DBSemaphore(String tablePrefix, String schedName, String defaultSQL, String defaultInsertSQL) {
this.tablePrefix = tablePrefix;
this.schedName = schedName;
setSQL(defaultSQL);
setInsertSQL(defaultInsertSQL);
}
在setSQL()和setInsertSQL()中为expandedSQL和 expandedInsertSQL赋值。
执行的SQL语句:
select * from QRTZ_LOCKS t where t.lock_name='TRIGGER_ACCESS' for update
在我们执行官方的建表脚本的时候,QRTZ_LOCKS表,它会为每个调度器创建两行数据,获取Trigger和触发Trigger是两把锁:
一个触发器只能绑定一个Job,但是一个Job可以有多个触发器
调度器线程执行的时候,首先从triggers表中获取状态为WAITING,并且将要触发的Trigger。然后将WAITING状态更新为ACQUIRED,表示该触发器抢占到了,防止其他调度器(实例)抢占。然后插入触发器信息以及实例名到FRIED_TRIGGERS表中,状态为ACQUIRED。前面的更新和后面的插入是在一个事务中进行的。
该触发器抢占到任务后,等待触发时间的到来。
执行时间到来后,每触发一次任务,都会在FIRED_TRIGGERS表中创建一条记录,并且状态为EXECUTING。如果任务允许并发执行,此时TRIGGERS表里的状态更新为WAITING,PAUSED,COMPLETE(不需要执行)。
如果任务不允许并发执行,还会把Triggers表里的状态更新为BLOCK或PAUSED_BLOCK。
Triggers表更新时根据 任务名和任务所属组名 而不是触发器名称和触发器组名来更新的。这就解决了一个任务有多个触发器的并发问题;然后触发器线程会创建一个执行环境来执行任务,以便在任务执行完成后更新触发器的状态。任务执行完成后,在一个事务中触发器状态更新为WAITING,删除FIRED_TRIGGERS表里对应的记录