项目中使用Quartz来管理定时任务,配置将任务信息保存到关系型数据库中,并且使用的是集群模式。集群会保证同一个任务到达触发时间的时候,只有一台机器去执行该任务。我现在想了解的是:
1.Quartz如何保证任务只在一个节点运行(即任务不会重复执行)?
2.在集群的哪个节点上运行,Quartz是如何进行选取的?
3.任务太多,而线程池中配置的线程太少时怎么办?
带着这三个问题去探究下Quartz内部的实现机制。
Quartz的基本概念:
简单介绍Quartz框架中的一些基本概念,详细的Quartz使用Demo另行百度把,一两句话也讲不清楚。
Scheduler:
Quartz中的定时调度器,用户将任务注册到Scheduler中进行调度
Job:
用户的定时任务
Trigger:
用户定时任务的触发策略
MisFire:
如果定时任务到了触发时间,但是由于一些其他意外原因没有触发,并且超过触发时间一定的时间范围(默认为60s),称之为MisFire。MisFire对应有多种处理策略,后续会讲到。
能想到的意外原因有:没有足够的线程运行任务、Schedule崩溃了(例如服务器断电或者进程崩溃了)、Job时间误设置成了很久之前的时间。
Quartz中的线程模型:
Quartz在运行时使用的是类似生产者和消费者模式,一个生产者去获取任务,然后将这些任务交给消费者去执行。Quartz中有如下几类线程:
1.线程池SimpleThreadPool和内部的工作线程WorkThread
Quartz内部自己实现的线程池,和JDK中的线程池很像,是运行具体任务用的。
2.核心调度线程QuartzScheduleThread
用于Trigger的获取、触发并放入到SimpleThreadPool线程池中去执行任务。
3.集群管理线程ClusterManager和处理MisFire任务线程MisfireHandler
开启cluster的话,前者用于check集群中的失效的节点,后续再细看这两个线程的内部原理把。本文暂时不去做详细的分析
Quartz线程模型如下图所示:
Quartz是如何保证任务只在一个节点上运行:
由于获取任务是由QuartzScheduleThread实现的,来看下它的run()方法是怎么获取Trigger然后执行的:
run()方法内部会不断看到如下这种判断,只要在执行的任意过程中发现Quartz实例已经停止了,那么任务就不再会继续执行了。
获取待触发的Triggers:
Trigger状态变成ACQUIRED,后文会细说下。
获取待触发的Triggers之后,需要检查下此时是否出现了触发时间更早的任务,如果存在需要判断是否要舍弃当前任务去获取更早的这些任务的Trigger来执行:isCandidateNewTimeEarlierWithinReason()方法内部有如下判断逻辑:
qsRsrcs.getJobStore().supportsPersistence() ? 70L : 7L
即如果使用的是数据库存储,查询时间假定为70ms,内存存储假定为7ms,如果当前时间距已获得的第一个Trigger触发时间小于这个查询时间,则认为丢弃当前已经获取到的Triggers是不合算的,会继续执行当前已获取到的Trigger。
通知JobStore修改已获取到的Trigger的状态:
将Trigger状态从ACQUIRED -> EXECUTING,表明Trigger已经被正确的被当前Quartz节点获取到并准备执行
如果操作Trigger时发生异常就释放掉Trigger,正常情况下会将Trigger对应的Job封装成JobRunShell然后扔到线程池中运行:
接下来,细看下从数据库中获取Trigger的流程:
triggers = qsRsrcs.getJobStore().acquireNextTriggers(now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow())
idleWaitTime默认是30s
qsRsrcs.getMaxBatchSize()默认是1
qsRsrcs.getBatchTimeWindow()默认是0
即调度线程一次会拉取NEXT_FIRE_TIME小于(now + idleWaitTime +batchTimeWindow),大于(now - misfireThreshold)的,min(availThreadCount,maxBatchSize)个triggers,默认情况下,如果Quartz有空闲的线程,会拉取未来30s,过去60s之间还未fire的1个trigger。
需要注意的是,如果不显示设置加锁并且获取的Trigger数量不超过1个,操作数据库时是不会加锁的(org.quartz.jobStore.acquireTriggersWithinLock=true):
后续的其他代码就是根据触发时间条件从数据库获取Triggers,主要就是SQL语句,暂时不打算细看了,如果有必要后续再仔细分析吧。
回答最开始提出来的那三个问题:
1.Quartz如何保证任务只在一个节点运行?
在acquireNextTriggers()方法中,通过数据库锁 + 事务来保证Trigger只被集群中的一个节点获取到
2.在集群的哪个节点上运行,Quartz是如何进行选取的?
随缘的。集群中各个节点做到时间同步很重要。如果待触发的任务少且运行快,那么很可能一直在时间最早的那一个节点上执行。
3.任务太多,而线程池中配置的线程太少时怎么办?
没事,反正选取Trigger的时候也会考虑空闲线程的数量,空闲线程少的话实例就少选取几个Trigger来执行。
现在想到的新问题:
单个节点在执行任务的过程中断电了,这些任务为什么设置recovery就会被重新调度执行,如果没有设置就直接是执行失败了。
后续有时间再继续分析吧…
附:正常情况下TriggerStatus的变换:
参考:
https://www.cnblogs.com/liuroy/p/7517777.html(Quartz中的线程模型图)
https://segmentfault.com/a/1190000015492260#articleHeader0 (Quartz任务重复调度问题)