quartz2.2.1集群调度机制调研及源码分析
引言
quartz集群架构
调度器实例化
调度过程
触发器的获取
触发trigger:
Job执行过程:
总结:
附:

引言

quratz是目前最为成熟,使用最广泛的java任务调度框架,功能强大配置灵活.在企业应用中占重要地位.quratz在集群环境中的使用方式是每个企业级系统都要考虑的问题.早在2006年,在ITeye上就有一篇关于quratz集群方案的讨论:http://www.iteye.com/topic/40970 ITeye创始人@Robbin在8楼给出了自己对quartz集群应用方案的意见.

后来有人总结了三种quratz集群方案:http://www.iteye.com/topic/114965

1.单独启动一个Job Server来跑job,不部署在web容器中.其他web节点当需要启动异步任务的时候,可以通过种种方式(DB,
JMS, Web Service, etc)通知Job Server,而Job
Server收到这个通知之后,把异步任务加载到自己的任务队列中去。

2.独立出一个job
server,这个server上跑一个spring+quartz的应用,这个应用专门用来启动任务。在jobserver上加上hessain,得到
业务接口,这样jobserver就可以调用web
container中的业务操作,也就是正真执行任务的还是在cluster中的tomcat。在jobserver启动定时任务之后,轮流调用各地址上
的业务操作(类似apache分发tomcat一样),这样可以让不同的定时任务在不同的节点上运行,减低了一台某个node的压力

3.quartz本身事实上也是支持集群的。在这种方案下,cluster上的每一个node都在跑quartz,然后也是通过数据中记录的状态来
判断这个操作是否正在执行,这就要求cluster上所有的node的时间应该是一样的。而且每一个node都跑应用就意味着每一个node都需要有自己
的线程池来跑quartz.

总的来说,第一种方法,在单独的server上执行任务,对任务的适用范围有很大的限制,要访问在web环境中的各种资源非常麻烦.但是集中式的管
理容易从架构上规避了分布式环境的种种同步问题.第二种方法在在第一种方法的基础上减轻了jobserver的重量,只发送调用请求,不直接执行任务,这
样解决了独立server无法访问web环境的问题,而且可以做到节点的轮询.可以有效地均衡负载.第三种方案是quartz自身支持的集群方案,在架构
上完全是分布式的,没有集中的管理,quratz通过数据库锁以及标识字段保证多个节点对任务不重复获取,并且有负载平衡机制和容错机制,用少量的冗余,
换取了高可用性(high avilable HA)和高可靠性.(个人认为和git的机制有异曲同工之处,分布式的冗余设计,换取可靠性和速度).

本文旨在研究quratz为解决分布式任务调度中存在的防止重复执行和负载均衡等问题而建立的机制.以调度流程作为顺序,配合源码理解其中原理.

quratz的配置,及具体应用请参考CRM项目组的另一篇文章:CRM使用Quartz集群总结分享

quartz集群架构

quartz的分布式架构如上图,可以看到数据库是各节点上调度器的枢纽.各个节点并不感知其他节点的存在,只是通过数据库来进行间接的沟通.

实际上,quartz的分布式策略就是一种以数据库作为边界资源的并发策略.每个节点都遵守相同的操作规范,使得对数据库的操作可以串行执行.而不同名称的调度器又可以互不影响的并行运行.

组件间的通讯图如下:(*注:主要的sql语句附在文章最后)

quartz运行时由QuartzSchedulerThread类作为主体,循环执行调度流程。JobStore作为中间层,按照quartz的
并发策略执行数据库操作,完成主要的调度逻辑。JobRunShellFactory负责实例化JobDetail对象,将其放入线程池运行。
LockHandler负责获取LOCKS表中的数据库锁。

整个quartz对任务调度的时序大致如下:

梳理一下其中的流程,可以表示为:

0.调度器线程run()

1.获取待触发trigger

1.1数据库LOCKS表TRIGGER_ACCESS行加锁

1.2读取JobDetail信息

1.3读取trigger表中触发器信息并标记为"已获取"

1.4commit事务,释放锁

2.触发trigger

2.1数据库LOCKS表STATE_ACCESS行加锁

2.2确认trigger的状态

2.3读取trigger的JobDetail信息

2.4读取trigger的Calendar信息

2.3更新trigger信息

2.3commit事务,释放锁

3实例化并执行Job

3.1从线程池获取线程执行JobRunShell的run方法

可以看到,这个过程中有两个相似的过程:同样是对数据表的更新操作,同样是在执行操作前获取锁 操作完成后释放锁.这一规则可以看做是quartz解决集群问题的核心思想.

规则流程图:

进一步解释这条规则就是:一个调度器实例在执行涉及到分布式问题的数据库操作前,首先要获取QUARTZ2_LOCKS表中对应当前调度器的行级锁,获取锁后即可执行其他表中的数据库操作,随着操作事务的提交,行级锁被释放,供其他调度器实例获取.

集群中的每一个调度器实例都遵循这样一种严格的操作规程,那么对于同一类调度器来说,每个实例对数据库的操作只能是串行的.而不同名的调度器之间却可以并行执行.

下面我们深入源码,从微观上观察quartz集群调度的细节

调度器实例化

一个最简单的quartz helloworld应用如下:

  1. public class HelloWorldMain {
  2. Log log = LogFactory.getLog(HelloWorldMain.class);
  3.  
  4. public void run() {
  5. try {
  6. //取得Schedule对象
  7. SchedulerFactory sf = new StdSchedulerFactory();
  8. Scheduler sch = sf.getScheduler();
  9.  
  10. JobDetail jd = new JobDetail("HelloWorldJobDetail",Scheduler.DEFAULT_GROUP,HelloWorldJob.class);
  11. Trigger tg = TriggerUtils.makeMinutelyTrigger(1);
  12. tg.setName("HelloWorldTrigger");
  13.  
  14. sch.scheduleJob(jd, tg);
  15. sch.start();
  16. } catch ( Exception e ) {
  17. e.printStackTrace();
  18.  
  19. }
  20. }
  21. public static void main(String[] args) {
  22. HelloWorldMain hw = new HelloWorldMain();
  23. hw.run();
  24. }
  25. }

我们看到初始化一个调度器需要用工厂类获取实例:

SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sch = sf.getScheduler(); 

然后启动:

sch.start();
  1. 下面跟进StdSchedulerFactorygetScheduler()方法:
  1. public Scheduler getScheduler() throws SchedulerException {
  2. if (cfg == null) {
  3. initialize();
  4. }
  5. SchedulerRepository schedRep = SchedulerRepository.getInstance();
  6. //从"调度器仓库"中根据properties的SchedulerName配置获取一个调度器实例
  7. Scheduler sched = schedRep.lookup(getSchedulerName());
  8. if (sched != null) {
  9. if (sched.isShutdown()) {
  10. schedRep.remove(getSchedulerName());
  11. } else {
  12. return sched;
  13. }
  14. }
  15. //初始化调度器
  16. sched = instantiate();
  17. return sched;
  18. }

跟进初始化调度器方法sched = instantiate();发现是一个700多行的初始化方法,涉及到

    • 读取配置资源,
    • 生成QuartzScheduler对象,
    • 创建该对象的运行线程,并启动线程;
    • 初始化JobStore,QuartzScheduler,DBConnectionManager等重要组件,
      至此,调度器的初始化工作已完成,初始化工作中quratz读取了数据库中存放的对应当前调度器的锁信息,对应CRM中的表QRTZ2_LOCKS,中的STATE_ACCESS,TRIGGER_ACCESS两个LOCK_NAME.
  1. public void initialize(ClassLoadHelper loadHelper,
  2. SchedulerSignaler signaler) throws SchedulerConfigException {
  3. if (dsName == null) {
  4. throw new SchedulerConfigException("DataSource name not set.");
  5. }
  6. classLoadHelper = loadHelper;
  7. if(isThreadsInheritInitializersClassLoadContext()) {
  8. log.info("JDBCJobStore threads will inherit ContextClassLoader of thread: " + Thread.currentThread().getName());
  9. initializersLoader = Thread.currentThread().getContextClassLoader();
  10. }
  11.  
  12. this.schedSignaler = signaler;
  13. // If the user hasn't specified an explicit lock handler, then
  14. // choose one based on CMT/Clustered/UseDBLocks.
  15. if (getLockHandler() == null) {
  16.  
  17. // If the user hasn't specified an explicit lock handler,
  18. // then we *must* use DB locks with clustering
  19. if (isClustered()) {
  20. setUseDBLocks(true);
  21. }
  22.  
  23. if (getUseDBLocks()) {
  24. if(getDriverDelegateClass() != null && getDriverDelegateClass().equals(MSSQLDelegate.class.getName())) {
  25. if(getSelectWithLockSQL() == null) {
  26. //读取数据库LOCKS表中对应当前调度器的锁信息
  27. String msSqlDflt = "SELECT * FROM {0}LOCKS WITH (UPDLOCK,ROWLOCK) WHERE " + COL_SCHEDULER_NAME + " = {1} AND LOCK_NAME = ?";
  28. getLog().info("Detected usage of MSSQLDelegate class - defaulting 'selectWithLockSQL' to '" + msSqlDflt + "'.");
  29. setSelectWithLockSQL(msSqlDflt);
  30. }
  31. }
  32. getLog().info("Using db table-based data access locking (synchronization).");
  33. setLockHandler(new StdRowLockSemaphore(getTablePrefix(), getInstanceName(), getSelectWithLockSQL()));
  34. } else {
  35. getLog().info(
  36. "Using thread monitor-based data access locking (synchronization).");
  37. setLockHandler(new SimpleSemaphore());
  38. }
  39. }
  40. }

当调用sch.start();方法时,scheduler做了如下工作:

1.通知listener开始启动

2.启动调度器线程

3.启动plugin

4.通知listener启动完成

  1. public void start() throws SchedulerException {
  2. if (shuttingDown|| closed) {
  3. throw new SchedulerException(
  4. "The Scheduler cannot be restarted after shutdown() has been called.");
  5. }
  6. // QTZ-212 : calling new schedulerStarting() method on the listeners
  7. // right after entering start()
  8. //通知该调度器的listener启动开始
  9. notifySchedulerListenersStarting();
  10. if (initialStart == null) {
  11. initialStart = new Date();
  12. //启动调度器的线程
  13. this.resources.getJobStore().schedulerStarted();
  14. //启动plugins
  15. startPlugins();
  16. } else {
  17. resources.getJobStore().schedulerResumed();
  18. }
  19. schedThread.togglePause(false);
  20. getLog().info(
  21. "Scheduler " + resources.getUniqueIdentifier() + " started.");
  22. //通知该调度器的listener启动完成
  23. notifySchedulerListenersStarted();
  24. }

调度过程

调度器启动后,调度器的线程就处于运行状态了,开始执行quartz的主要工作–调度任务.

前面已介绍过,任务的调度过程大致分为三步:

1.获取待触发trigger

2.触发trigger

3.实例化并执行Job

下面分别分析三个阶段的源码.

QuartzSchedulerThread是调度器线程类,调度过程的三个步骤就承载在run()方法中,分析见代码注释:

  1. public void run() {
  2. boolean lastAcquireFailed = false;
  3. //
  4. while (!halted.get()) {
  5. try {
  6. // check if we're supposed to pause...
  7. synchronized (sigLock) {
  8. while (paused && !halted.get()) {
  9. try {
  10. // wait until togglePause(false) is called...
  11. sigLock.wait(1000L);
  12. } catch (InterruptedException ignore) {
  13. }
  14. }
  15. if (halted.get()) {
  16. break;
  17. }
  18. }
  19. /获取当前线程池中线程的数量
  20. int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
  21. if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...
  22. List<OperableTrigger> triggers = null;
  23. long now = System.currentTimeMillis();
  24. clearSignaledSchedulingChange();
  25. try {
  26. //调度器在trigger队列中寻找30秒内一定数目的trigger准备执行调度,
  27. //参数1:nolaterthan = now+3000ms,参数2 最大获取数量,大小取线程池线程剩余量与定义值得较小者
  28. //参数3 时间窗口 默认为0,程序会在nolaterthan后加上窗口大小来选择trigger
  29. triggers = qsRsrcs.getJobStore().acquireNextTriggers(
  30. now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
  31. //上一步获取成功将失败标志置为false;
  32. lastAcquireFailed = false;
  33. if (log.isDebugEnabled())
  34. log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
  35. } catch (JobPersistenceException jpe) {
  36. if(!lastAcquireFailed) {
  37. qs.notifySchedulerListenersError(
  38. "An error occurred while scanning for the next triggers to fire.",
  39. jpe);
  40. }
  41. //捕捉到异常则值标志为true,再次尝试获取
  42. lastAcquireFailed = true;
  43. continue;
  44. } catch (RuntimeException e) {
  45. if(!lastAcquireFailed) {
  46. getLog().error("quartzSchedulerThreadLoop: RuntimeException "
  47. +e.getMessage(), e);
  48. }
  49. lastAcquireFailed = true;
  50. continue;
  51. }
  52. if (triggers != null && !triggers.isEmpty()) {
  53. now = System.currentTimeMillis();
  54. long triggerTime = triggers.get(0).getNextFireTime().getTime();
  55. long timeUntilTrigger = triggerTime - now;//计算距离trigger触发的时间
  56. while(timeUntilTrigger > 2) {
  57. synchronized (sigLock) {
  58. if (halted.get()) {
  59. break;
  60. }
  61. //如果这时调度器发生了改变,新的trigger添加进来,那么有可能新添加的trigger比当前待执行的trigger
  62. //更急迫,那么需要放弃当前trigger重新获取,然而,这里存在一个值不值得的问题,如果重新获取新trigger
  63. //的时间要长于当前时间到新trigger出发的时间,那么即使放弃当前的trigger,仍然会导致xntrigger获取失败,
  64. //但我们又不知道获取新的trigger需要多长时间,于是,我们做了一个主观的评判,若jobstore为RAM,那么
  65. //假定获取时间需要7ms,若jobstore是持久化的,假定其需要70ms,当前时间与新trigger的触发时间之差小于
  66. // 这个值的我们认为不值得重新获取,返回false
  67. //这里判断是否有上述情况发生,值不值得放弃本次trigger,若判定不放弃,则线程直接等待至trigger触发的时刻
  68. if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
  69. try {
  70. // we could have blocked a long while
  71. // on 'synchronize', so we must recompute
  72. now = System.currentTimeMillis();
  73. timeUntilTrigger = triggerTime - now;
  74. if(timeUntilTrigger >= 1)
  75. sigLock.wait(timeUntilTrigger);
  76. } catch (InterruptedException ignore) {
  77. }
  78. }
  79. }
  80. //该方法调用了上面的判定方法,作为再次判定的逻辑
  81. //到达这里有两种情况1.决定放弃当前trigger,那么再判定一次,如果仍然有放弃,那么清空triggers列表并
  82. // 退出循环 2.不放弃当前trigger,且线程已经wait到trigger触发的时刻,那么什么也不做
  83. if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
  84. break;
  85. }
  86. now = System.currentTimeMillis();
  87. timeUntilTrigger = triggerTime - now;
  88. //这时触发器已经即将触发,值会<2
  89. }
  90. // this happens if releaseIfScheduleChangedSignificantly decided to release triggers
  91. if(triggers.isEmpty())
  92. continue;
  93. // set triggers to 'executing'
  94. List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();
  95. boolean goAhead = true;
  96. synchronized(sigLock) {
  97. goAhead = !halted.get();
  98. }
  99. if(goAhead) {
  100. try {
  101. //触发triggers,结果付给bndles,注意,从这里返回后,trigger在数据库中已经经过了锁定,解除锁定,这一套过程
  102. //所以说,quratz定不是等到job执行完才释放trigger资源的占有,而是读取完本次触发所需的信息后立即释放资源
  103. //然后再执行jobs
  104. List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
  105. if(res != null)
  106. bndles = res;
  107. } catch (SchedulerException se) {
  108. qs.notifySchedulerListenersError(
  109. "An error occurred while firing triggers '"
  110. + triggers + "'", se);
  111. //QTZ-179 : a problem occurred interacting with the triggers from the db
  112. //we release them and loop again
  113. for (int i = 0; i < triggers.size(); i++) {
  114. qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
  115. }
  116. continue;
  117. }
  118. }
  119. //迭代trigger的信息,分别跑job
  120. for (int i = 0; i < bndles.size(); i++) {
  121. TriggerFiredResult result = bndles.get(i);
  122. TriggerFiredBundle bndle = result.getTriggerFiredBundle();
  123. Exception exception = result.getException();
  124. if (exception instanceof RuntimeException) {
  125. getLog().error("RuntimeException while firing trigger " + triggers.get(i), exception);
  126. qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
  127. continue;
  128. }
  129. // it's possible to get 'null' if the triggers was paused,
  130. // blocked, or other similar occurrences that prevent it being
  131. // fired at this time... or if the scheduler was shutdown (halted)
  132. //在特殊情况下,bndle可能为null,看triggerFired方法可以看到,当从数据库获取trigger时,如果status不是
  133. //STATE_ACQUIRED,那么会直接返回空.quratz这种情况下本调度器启动重试流程,重新获取4次,若仍有问题,
  134. // 则抛出异常.
  135. if (bndle == null) {
  136. qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
  137. continue;
  138. }
  139. //执行job
  140. JobRunShell shell = null;
  141. try {
  142. //创建一个job的Runshell
  143. shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
  144. shell.initialize(qs);
  145. } catch (SchedulerException se) {
  146. qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
  147. continue;
  148. }
  149. //把runShell放在线程池里跑
  150. if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
  151. // this case should never happen, as it is indicative of the
  152. // scheduler being shutdown or a bug in the thread pool or
  153. // a thread pool being used concurrently - which the docs
  154. // say not to do...
  155. getLog().error("ThreadPool.runInThread() return false!");
  156. qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
  157. }
  158. }
  159. continue; // while (!halted)
  160. }
  161. } else { // if(availThreadCount > 0)
  162. // should never happen, if threadPool.blockForAvailableThreads() follows contract
  163. continue; // while (!halted)
  164. }
  165. //保证负载平衡的方法,每次执行一轮触发后,本scheduler会等待一个随机的时间,这样就使得其他节点上的scheduler可以得到资源.
  166. long now = System.currentTimeMillis();
  167. long waitTime = now + getRandomizedIdleWaitTime();
  168. long timeUntilContinue = waitTime - now;
  169. synchronized(sigLock) {
  170. try {
  171. if(!halted.get()) {
  172. // QTZ-336 A job might have been completed in the mean time and we might have
  173. // missed the scheduled changed signal by not waiting for the notify() yet
  174. // Check that before waiting for too long in case this very job needs to be
  175. // scheduled very soon
  176. if (!isScheduleChanged()) {
  177. sigLock.wait(timeUntilContinue);
  178. }
  179. }
  180. } catch (InterruptedException ignore) {
  181. }
  182. }
  183. } catch(RuntimeException re) {
  184. getLog().error("Runtime error occurred in main trigger firing loop.", re);
  185. }
  186. } // while (!halted)
  187. // drop references to scheduler stuff to aid garbage collection...
  188. qs = null;
  189. qsRsrcs = null;
  190. }

调度器每次获取到的trigger是30s内需要执行的,所以要等待一段时间至trigger执行前2ms.在等待过程中涉及到一个新加进来更紧急的trigger的处理逻辑.分析写在注释中,不再赘述.

可以看到调度器的只要在运行状态,就会不停地执行调度流程.值得注意的是,在流程的最后线程会等待一个随机的时间.这就是quartz自带的负载平衡机制.

以下是三个步骤的跟进:

触发器的获取

调度器调用:

triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());

在数据库中查找一定时间范围内将会被触发的trigger.参数的意义如下:参数1:nolaterthan = now+3000ms,即未来30s内将会被触发.参数2 最大获取数量,大小取线程池线程剩余量与定义值得较小者.参数3 时间窗口 默认为0,程序会在nolaterthan后加上窗口大小来选择trigger.quratz会在每次触发trigger后计算出trigger下次要执 行的时间,并在数据库QRTZ2_TRIGGERS中的NEXT_FIRE_TIME字段中记录.查找时将当前毫秒数与该字段比较,就能找出下一段时间内 将会触发的触发器.查找时,调用在JobStoreSupport类中的方法:

  1. public List<OperableTrigger> acquireNextTriggers(final long noLaterThan, final int maxCount, final long timeWindow)
  2. throws JobPersistenceException {
  3.  
  4. String lockName;
  5. if(isAcquireTriggersWithinLock() || maxCount > 1) {
  6. lockName = LOCK_TRIGGER_ACCESS;
  7. } else {
  8. lockName = null;
  9. }
  10. return executeInNonManagedTXLock(lockName,
  11. new TransactionCallback<List<OperableTrigger>>() {
  12. public List<OperableTrigger> execute(Connection conn) throws JobPersistenceException {
  13. return acquireNextTrigger(conn, noLaterThan, maxCount, timeWindow);
  14. }
  15. },
  16. new TransactionValidator<List<OperableTrigger>>() {
  17. public Boolean validate(Connection conn, List<OperableTrigger> result) throws JobPersistenceException {
  18. //...异常处理回调方法
  19. }
  20. });
  21. }

该方法关键的一点在于执行了executeInNonManagedTXLock()方法,这一方法指定了一个锁名,两个回调函数.在开始执行时获 得锁,在方法执行完毕后随着事务的提交锁被释放.在该方法的底层,使用 for update语句,在数据库中加入行级锁,保证了在该方法执行过程中,其他的调度器对trigger进行获取时将会等待该调度器释放该锁.此方法是前面介 绍的quartz集群策略的的具体实现,这一模板方法在后面的trigger触发过程还会被使用.

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";

进一步解释:quratz在获取数据库资源之前,先要以for update方式访问LOCKS表中相应LOCK_NAME数据将改行锁定.如果在此前该行已经被锁定,那么等待,如果没有被锁定,那么读取满足要求的 trigger,并把它们的status置为STATE_ACQUIRED,如果有tirgger已被置为STATE_ACQUIRED,那么说明该 trigger已被别的调度器实例认领,无需再次认领,调度器会忽略此trigger.调度器实例之间的间接通信就体现在这里.

JobStoreSupport.acquireNextTrigger()方法中:

int rowsUpdated = getDelegate().updateTriggerStateFromOtherState(conn, triggerKey, STATE_ACQUIRED, STATE_WAITING);

最后释放锁,这时如果下一个调度器在排队获取trigger的话,则仍会执行相同的步骤.这种机制保证了trigger不会被重复获取.按照这种算法正常运行状态下调度器每次读取的trigger中会有相当一部分已被标记为被获取.

获取trigger的过程进行完毕.

触发trigger:

QuartzSchedulerThread line336:

List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);

调用JobStoreSupport类的triggersFired()方法:

  1. public List<TriggerFiredResult> triggersFired(final List<OperableTrigger> triggers) throws JobPersistenceException {
  2. return executeInNonManagedTXLock(LOCK_TRIGGER_ACCESS,
  3. new TransactionCallback<List<TriggerFiredResult>>() {
  4. public List<TriggerFiredResult> execute(Connection conn) throws JobPersistenceException {
  5. List<TriggerFiredResult> results = new ArrayList<TriggerFiredResult>();
  6. TriggerFiredResult result;
  7. for (OperableTrigger trigger : triggers) {
  8. try {
  9. TriggerFiredBundle bundle = triggerFired(conn, trigger);
  10. result = new TriggerFiredResult(bundle);
  11. } catch (JobPersistenceException jpe) {
  12. result = new TriggerFiredResult(jpe);
  13. } catch(RuntimeException re) {
  14. result = new TriggerFiredResult(re);
  15. }
  16. results.add(result);
  17. }
  18. return results;
  19. }
  20. },
  21. new TransactionValidator<List<TriggerFiredResult>>() {
  22. @Override
  23. public Boolean validate(Connection conn, List<TriggerFiredResult> result) throws JobPersistenceException {
  24. //...异常处理回调方法
  25. }
  26. });
  27. }

此处再次用到了quratz的行为规范:executeInNonManagedTXLock()方法,在获取锁的情况下对trigger进行触发操作.其中的触发细节如下:

  1. protected TriggerFiredBundle triggerFired(Connection conn,
  2. OperableTrigger trigger)
  3. throws JobPersistenceException {
  4. JobDetail job;
  5. Calendar cal = null;
  6. // Make sure trigger wasn't deleted, paused, or completed...
  7. try { // if trigger was deleted, state will be STATE_DELETED
  8. String state = getDelegate().selectTriggerState(conn,
  9. trigger.getKey());
  10. if (!state.equals(STATE_ACQUIRED)) {
  11. return null;
  12. }
  13. } catch (SQLException e) {
  14. throw new JobPersistenceException("Couldn't select trigger state: "
  15. + e.getMessage(), e);
  16. }
  17. try {
  18. job = retrieveJob(conn, trigger.getJobKey());
  19. if (job == null) { return null; }
  20. } catch (JobPersistenceException jpe) {
  21. try {
  22. getLog().error("Error retrieving job, setting trigger state to ERROR.", jpe);
  23. getDelegate().updateTriggerState(conn, trigger.getKey(),
  24. STATE_ERROR);
  25. } catch (SQLException sqle) {
  26. getLog().error("Unable to set trigger state to ERROR.", sqle);
  27. }
  28. throw jpe;
  29. }
  30. if (trigger.getCalendarName() != null) {
  31. cal = retrieveCalendar(conn, trigger.getCalendarName());
  32. if (cal == null) { return null; }
  33. }
  34. try {
  35. getDelegate().updateFiredTrigger(conn, trigger, STATE_EXECUTING, job);
  36. } catch (SQLException e) {
  37. throw new JobPersistenceException("Couldn't insert fired trigger: "
  38. + e.getMessage(), e);
  39. }
  40. Date prevFireTime = trigger.getPreviousFireTime();
  41. // call triggered - to update the trigger's next-fire-time state...
  42. trigger.triggered(cal);
  43. String state = STATE_WAITING;
  44. boolean force = true;
  45.  
  46. if (job.isConcurrentExectionDisallowed()) {
  47. state = STATE_BLOCKED;
  48. force = false;
  49. try {
  50. getDelegate().updateTriggerStatesForJobFromOtherState(conn, job.getKey(),
  51. STATE_BLOCKED, STATE_WAITING);
  52. getDelegate().updateTriggerStatesForJobFromOtherState(conn, job.getKey(),
  53. STATE_BLOCKED, STATE_ACQUIRED);
  54. getDelegate().updateTriggerStatesForJobFromOtherState(conn, job.getKey(),
  55. STATE_PAUSED_BLOCKED, STATE_PAUSED);
  56. } catch (SQLException e) {
  57. throw new JobPersistenceException(
  58. "Couldn't update states of blocked triggers: "
  59. + e.getMessage(), e);
  60. }
  61. }
  62.  
  63. if (trigger.getNextFireTime() == null) {
  64. state = STATE_COMPLETE;
  65. force = true;
  66. }
  67. storeTrigger(conn, trigger, job, true, state, force, false);
  68. job.getJobDataMap().clearDirtyFlag();
  69. return new TriggerFiredBundle(job, trigger, cal, trigger.getKey().getGroup()
  70. .equals(Scheduler.DEFAULT_RECOVERY_GROUP), new Date(), trigger
  71. .getPreviousFireTime(), prevFireTime, trigger.getNextFireTime());
  72. }

该方法做了以下工作:

1.获取trigger当前状态

2.通过trigger中的JobKey读取trigger包含的Job信息

3.将trigger更新至触发状态

4.结合calendar的信息触发trigger,涉及多次状态更新

5.更新数据库中trigger的信息,包括更改状态至STATE_COMPLETE,及计算下一次触发时间.

6.返回trigger触发结果的数据传输类TriggerFiredBundle

从该方法返回后,trigger的执行过程已基本完毕.回到执行quratz操作规范的executeInNonManagedTXLock方法,将数据库锁释放.

trigger触发操作完成

Job执行过程:

再回到线程类QuartzSchedulerThread的 line353这时触发器都已出发完毕,job的详细信息都已就位

QuartzSchedulerThread line:368

qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
shell.initialize(qs);

为每个Job生成一个可运行的RunShell,并放入线程池运行.

在最后调度线程生成了一个随机的等待时间,进入短暂的等待,这使得其他节点的调度器都有机会获取数据库资源.如此就实现了quratz的负载平衡.

这样一次完整的调度过程就结束了.调度器线程进入下一次循环.

总结:

简单地说,quartz的分布式调度策略是以数据库为边界资源的一种异步策略.各个调度器都遵守一个基于数据库锁的操作规则保证了操作的唯一性.同时多个节点的异步运行保证了服务的可靠.但这种策略有自己的局限性.摘录官方文档中对quratz集群特性的说明:

Only one node will fire the job for each firing. What I mean by that is, if the job has a repeating trigger that tells it to fire every 10 seconds, then at 12:00:00 exactly one node will run the job, and at 12:00:10 exactly one node will run the job, etc. It won't necessarily be the same node each time - it will more or less be random which node runs it. The load balancing mechanism is near-random for busy schedulers (lots of triggers) but favors the same node for non-busy (e.g. few triggers) schedulers.

The clustering feature works best for scaling out long-running and/or cpu-intensive jobs (distributing the work-load over multiple nodes). If you need to scale out to support thousands of short-running (e.g 1 second) jobs, consider partitioning the set of jobs by using multiple distinct schedulers (including multiple clustered schedulers for HA). The scheduler makes use of a cluster-wide lock, a pattern that degrades performance as you add more nodes (when going beyond about three nodes - depending upon your database's capabilities, etc.).

说明指出,集群特性对于高cpu使用率的任务效果很好,但是对于大量的短任务,各个节点都会抢占数据库锁,这样就出现大量的线程等待资源.这种情况随着节点的增加会越来越严重.

附:

通讯图中关键步骤的主要sql语句:

  1. 3.
  2. select TRIGGER_ACCESS from QRTZ2_LOCKS for update
  3. 4.
  4. SELECT TRIGGER_NAME,
  5. TRIGGER_GROUP,
  6. NEXT_FIRE_TIME,
  7. PRIORITY
  8. FROM QRTZ2_TRIGGERS
  9. WHERE SCHEDULER_NAME = 'CRMscheduler'
  10. AND TRIGGER_STATE = 'ACQUIRED'
  11. AND NEXT_FIRE_TIME <= '{timekey 30s latter}'
  12. AND ( MISFIRE_INSTR = -1
  13. OR ( MISFIRE_INSTR != -1
  14. AND NEXT_FIRE_TIME >= '{timekey now}' ) )
  15. ORDER BY NEXT_FIRE_TIME ASC,
  16. PRIORITY DESC;
  17. 5.
  18. SELECT *
  19. FROM QRTZ2_JOB_DETAILS
  20. WHERE SCHEDULER_NAME = CRMscheduler
  21. AND JOB_NAME = ?
  22. AND JOB_GROUP = ?;
  23. 6.
  24. UPDATE TQRTZ2_TRIGGERS
  25. SET TRIGGER_STATE = 'ACQUIRED'
  26. WHERE SCHED_NAME = 'CRMscheduler'
  27. AND TRIGGER_NAME = '{triggerName}'
  28. AND TRIGGER_GROUP = '{triggerGroup}'
  29. AND TRIGGER_STATE = 'waiting';
  30. 7.
  31. INSERT INTO QRTZ2_FIRED_TRIGGERS
  32. (SCHEDULER_NAME,
  33. ENTRY_ID,
  34. TRIGGER_NAME,
  35. TRIGGER_GROUP,
  36. INSTANCE_NAME,
  37. FIRED_TIME,
  38. SCHED_TIME,
  39. STATE,
  40. JOB_NAME,
  41. JOB_GROUP,
  42. IS_NONCONCURRENT,
  43. REQUESTS_RECOVERY,
  44. PRIORITY)
  45. VALUES( 'CRMscheduler', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
  46. 8.
  47. commit;
  48. 12.
  49. select STAT_ACCESS from QRTZ2_LOCKS for update
  50. 13.
  51. SELECT TRIGGER_STATE FROM QRTZ2_TRIGGERS WHERE SCHEDULER_NAME = 'CRMscheduler' AND TRIGGER_NAME = ? AND TRIGGER_GROUP = ?;
  52. 14.
  53. SELECT TRIGGER_STATE
  54. FROM QRTZ2_TRIGGERS
  55. WHERE SCHEDULER_NAME = 'CRMscheduler'
  56. AND TRIGGER_NAME = ?
  57. AND TRIGGER_GROUP = ?;
  58. 14.
  59. SELECT *
  60. FROM QRTZ2_JOB_DETAILS
  61. WHERE SCHEDULER_NAME = CRMscheduler
  62. AND JOB_NAME = ?
  63. AND JOB_GROUP = ?;
  64. 15.
  65. SELECT *
  66. FROM QRTZ2_CALENDARS
  67. WHERE SCHEDULER_NAME = 'CRMscheduler'
  68. AND CALENDAR_NAME = ?;
  69. 16.
  70. UPDATE QRTZ2_FIRED_TRIGGERS
  71. SET INSTANCE_NAME = ?,
  72. FIRED_TIME = ?,
  73. SCHED_TIME = ?,
  74. ENTRY_STATE = ?,
  75. JOB_NAME = ?,
  76. JOB_GROUP = ?,
  77. IS_NONCONCURRENT = ?,
  78. REQUESTS_RECOVERY = ?
  79. WHERE SCHEDULER_NAME = 'CRMscheduler'
  80. AND ENTRY_ID = ?;
  81. 17.
  82. UPDATE TQRTZ2_TRIGGERS
  83. SET TRIGGER_STATE = ?
  84. WHERE SCHED_NAME = 'CRMscheduler'
  85. AND TRIGGER_NAME = '{triggerName}'
  86. AND TRIGGER_GROUP = '{triggerGroup}'
  87. AND TRIGGER_STATE = ?;
  88. 18.
  89. UPDATE QRTZ2_TRIGGERS
  90. SET JOB_NAME = ?,
  91. JOB_GROUP = ?,
  92. DESCRIPTION = ?,
  93. NEXT_FIRE_TIME = ?,
  94. PREV_FIRE_TIME = ?,
  95. TRIGGER_STATE = ?,
  96. TRIGGER_TYPE = ?,
  97. START_TIME = ?,
  98. END_TIME = ?,
  99. CALENDAR_NAME = ?,
  100. MISFIRE_INSTRUCTION = ?,
  101. PRIORITY = ?,
  102. JOB_DATAMAP = ?
  103. WHERE SCHEDULER_NAME = SCHED_NAME_SUBST
  104. AND TRIGGER_NAME = ?
  105. AND TRIGGER_GROUP = ?;
  106. 19.
  107. commit;

原文地址:http://demo.netfoucs.com/gklifg/article/details/27090179

定时组件quartz系列<三>quartz调度机制调研及源码分析的更多相关文章

  1. quartz集群调度机制调研及源码分析---转载

    quartz2.2.1集群调度机制调研及源码分析引言quartz集群架构调度器实例化调度过程触发器的获取触发trigger:Job执行过程:总结:附: 引言 quratz是目前最为成熟,使用最广泛的j ...

  2. (1)quartz集群调度机制调研及源码分析---转载

    quartz2.2.1集群调度机制调研及源码分析 原文地址:http://demo.netfoucs.com/gklifg/article/details/27090179 引言quartz集群架构调 ...

  3. 一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  4. 深入理解分布式调度框架TBSchedule及源码分析

    简介 由于最近工作比较忙,前前后后花了两个月的时间把TBSchedule的源码翻了个底朝天.关于TBSchedule的使用,网上也有很多参考资料,这里不做过多的阐述.本文着重介绍TBSchedule的 ...

  5. 第三篇:Spark SQL Catalyst源码分析之Analyzer

    /** Spark SQL源码分析系列文章*/ 前面几篇文章讲解了Spark SQL的核心执行流程和Spark SQL的Catalyst框架的Sql Parser是怎样接受用户输入sql,经过解析生成 ...

  6. Netty之旅三:Netty服务端启动源码分析,一梭子带走!

    Netty服务端启动流程源码分析 前记 哈喽,自从上篇<Netty之旅二:口口相传的高性能Netty到底是什么?>后,迟迟两周才开启今天的Netty源码系列.源码分析的第一篇文章,下一篇我 ...

  7. Bytom Dapp 开发笔记(三):Dapp Demo前端源码分析

    本章内容会针对比原官方提供的dapp-demo,分析里面的前端源码,分析清楚整个demo的流程,然后针对里面开发过程遇到的坑,添加一下个人的见解还有解决的方案. 储蓄分红合约简述 为了方便理解,这里简 ...

  8. leaflet-webpack 入门开发系列三地图分屏对比(附源码下载)

    前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...

  9. Cocos2d-X3.0 刨根问底(六)----- 调度器Scheduler类源码分析

    上一章,我们分析Node类的源码,在Node类里面耦合了一个 Scheduler 类的对象,这章我们就来剖析Cocos2d-x的调度器 Scheduler 类的源码,从源码中去了解它的实现与应用方法. ...

随机推荐

  1. Map中放置类指针并实现调用

    工作中使用到多进程通信,利用到了map以及multimap来进行实现. 需要做一个简单测试例子,直接上代码. /* * main.cpp * Created on: Oct 28, 2013 * Au ...

  2. UVA 11076 Add Again 计算对答案的贡献+组合数学

    A pair of numbers has a unique LCM but a single number can be the LCM of more than one possiblepairs ...

  3. C Primer Plus之结构和其他数据形式

    声明和初始化结构指针 声明结构化指针,例如: struct guy * him; 初始化结构指针(如果barney是一个guy类型的结构),例如: him = &barney; 注意:和数组不 ...

  4. QTP10补丁汇总

    QTP10补丁汇总 QTP_00591.EXE QTP10 调试器视图问题的补丁 QTP_00591 - Prevent QuickTest Debug Viewer Problems when Pr ...

  5. Spring REST for DELETE Request Method Not Supoorted

    http://stackoverflow.com/questions/22055251/sending-data-with-angularjs-http-delete-request I have a ...

  6. Hadoop基础教程-运行环境搭建

    一.Hadoop是什么 一个分布式系统基础架构,由Apache基金会所开发.用户可以在不了解分布式底层细节的情况下,开发分布式程序.充分利用集群的威力进行高速运算和存储. Hadoop实现了一个分布式 ...

  7. Qt中的多线程技术(列表总结比较,多线程创建和销毁其实是有开销的,只是增加了用户体验而已)

    http://blog.csdn.net/u011012932/article/details/52943811

  8. CentOS增加硬盘

    1.查看新硬盘     #fdisk –l      新添加的硬盘的编号为/dev/sdb 2.硬盘分区     1)进入fdisk模式     #/sbin/fdisk /dev/sdb     2 ...

  9. Android 音乐播放器之--错误状态下调用导致的异常

    MediaPlayer必须在合适的状态下调用合适的方法,否则会出现异常,下面列出常见错误信息和说明: 1.E/MediaPlayer(11310): stop called in state 1 调用 ...

  10. 【Netty学习】Netty 4.0.x版本和Flex 4.6配合

    笔者的男装网店:http://shop101289731.taobao.com .冬装,在寒冷的冬季温暖你.新品上市,环境选购 =================================不华丽 ...