【开发经验】quartz框架集群执行原理(源码解析)


前言

开发的过程中经常会遇到需要定时器的功能,例如:

  1. 每天晚上同步用户信息
  2. 每隔一段时间计算用户喜欢的类型
  3. 定时发送邮件
  4. 定时发送短信

等等。定时的框架也很多,但是较为轻量级的quartz一定可以占据一席之地。
       quartz是一个相对轻量级的定时器框架,可以在分布式环境中运行定时任务。在此,不讲如何使用quartz框架,更加着重阐述quartz框架如何更好的保证任务的运行。


一、Job Stores

       一个任务的执行时间,执行时需要调用哪个方法,都是需要保存的。Job Stores直译为工作存储,在quartz框架中是任务的保存方式。常见的有两种RAMJobStore(内存保存)和JDBC JobStore(数据库保存)。

  • RAMJobStore:是使用最简单的JobStore,它也是性能最高的(在CPU时间方面)。它将其所有数据保存在RAM中。这就是为什么它是闪电般快的,也是为什么这么简单的配置。缺点是当您的应用程序结束(或崩溃)时,所有调度信息都将丢失 - 这意味着RAMJobStore无法履行作业和triggers上的“非易失性”设置。对于某些应用程序,这是可以接受的 - 甚至是所需的行为,但对于其他应用程序,这可能是灾难性的。
  • JDBC JobStore:它通过JDBC将其所有数据保存在数据库中。因此,配置比RAMJobStore要复杂一点,而且也不是那么快。但是,性能下降并不是很糟糕,特别是如果您在主键上构建具有索引的数据库表。在相当现代的一套具有体面的LAN(在调度程序和数据库之间)的机器上,检索和更新触发triggers的时间通常将小于10毫秒。尽管有这么多的不好,但是这种方式可以保证任务以安全的方式运行。在创建定时任务时,也许不需要多高的效率,只需要保证任务稳定执行即可。

       一种是内存的,一种是数据库的,肯定还有中间版本,基于缓存的。TerracottaJobStore就是介于两者之间的一种存储介质。这种方式,可能用的少吧,有兴趣的可以百科一下。

1.1、JDBC JobStore 详解

       Quartz的集群功能通过故障转移和负载平衡功能为您的调度程序带来了高可用性和可扩展性。
在这里插入图片描述
       如上任务,简单的定时任务框架,如果在执行定时任务的时候,每个服务都会执行一次。另外,如果想在多服务下通过master选举的方式保证任务只执行一次的话,会有很多弊端,比如:所有的任务都在一个服务执行,造成任务运行缓慢,卡顿等现象。

在这里插入图片描述
       在如上这种架构时,quartz框架可以实现任务稳定的执行,并且任务的分配相对均衡(每天凌晨7点发送短信,可能会在服务A执行,每天8点发送头条新闻,可能会在服务B执行);可以实现故障转移(某个任务在执行的过程中挂掉,另一个服务会重新执行)。

二、思路

思路如下
第一步
       查询qrtz_triggers表获取将要执行的触发器,将他认定为马上要执行(插入qrtz_fired_triggers表一条记录)。QuartzSchedulerThread中搜索acquireNextTriggers方法可见。
       默认没有加悲观锁,但是可以通过配置使用悲观锁。,其中的操作有查询qrtz_triggers表,修改qrtz_triggers表,和插入qrtz_fired_triggers表,3次数据库操作,3次在同一个事务中。而且修改的时候也只修改状态为WAITING的数据,修改成功才插入,这样可以避免集群中插入重复数据。这样总感觉很是麻烦,为什么这么处理,下一篇说明。
第二步
       获取马上要执行的触发器,修改状态为执行中。triggersFired方法可见。
       将第一步插入的qrtz_fired_triggers的数据修改为EXECUTING状态。此过程由悲观锁保证线程安全。
第三步
       如果检验通过,则执行任务,执行完成,修改qrtz_triggers表记录,并且删除qrtz_fired_triggers表记录。JobRunShell .run方法可见。
执行对应的方法,因为已经执行完成,把qrtz_triggers表的数据修改为WAITING状态,并且删除qrtz_fired_triggers表的数据。与数据库交互的过程由悲观锁保证线程安全。

三、 源码解析

核心代码QuartzSchedulerThread.run方法。

QuartzSchedulerThread.run入口方法

//这个是线程启动的方法,当调用scheduler.start()的时候这里就开始执行了 。
     public void run() {
        int acquiresFailed = 0;
		
        while (!halted.get()) {
        // 这里锁的概念,说实话挺乱的,没太在意。
            try {
            //省略部分代码
              	//如果当前空余线程数大于0
                if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...

                    List<OperableTrigger> triggers;

                    long now = System.currentTimeMillis();

                    clearSignaledSchedulingChange();
                    try {
                    //查询qrtz_triggers表并且返回触发器信息。
                        triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                                now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
                        acquiresFailed = 0;
                        if (log.isDebugEnabled())
                            log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
                    }  
                    //省略部分代码

                    if (triggers != null && !triggers.isEmpty()) {

                        now = System.currentTimeMillis();
                        long triggerTime = triggers.get(0).getNextFireTime().getTime();
                        long timeUntilTrigger = triggerTime - now;
       //这个循环里面考虑了:在等待任务时间到达的过程中,
       //Scheduler状态发生变化,
       //例如,又新加了一个新的任务,新的任务执行时间可能比目前等待执行的时间早,
       //此时就应该考虑新加入的任务。
       //所以作者认为,只有去获取新trigger所花费的时间小于马上执
       //行还剩的时间的时候,才会抛弃当前trigger,去选择新来的trigger。
       //有点绕,这算是一种补偿机制吧。
                        while(timeUntilTrigger > 2) {
                            synchronized (sigLock) {
                                if (halted.get()) {
                                    break;
                                }
                                if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
                                    try {
                                        // we could have blocked a long while
                                        // on 'synchronize', so we must recompute
                                        now = System.currentTimeMillis();
                                        timeUntilTrigger = triggerTime - now;
                                        if(timeUntilTrigger >= 1)
                                            sigLock.wait(timeUntilTrigger);
                                    } catch (InterruptedException ignore) {
                                    }
                                }
                            }
                            if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
                                break;
                            }
                            now = System.currentTimeMillis();
                            timeUntilTrigger = triggerTime - now;
                        }

                        
                        if(goAhead) {
                            try {
                         //将触发器修改为执行中
                         //稍后有详细说明     
                           List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
                                if(res != null)
                                    bndles = res;
                            } catch (SchedulerException se) {
                                qs.notifySchedulerListenersError(
                                        "An error occurred while firing triggers '"
                                                + triggers + "'", se);
                                //QTZ-179 : a problem occurred interacting with the triggers from the db
                                //we release them and loop again
                                for (int i = 0; i < triggers.size(); i++) {
                                    qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                                }
                                continue;
                            }

                        }

                        for (int i = 0; i < bndles.size(); i++) {
                            TriggerFiredResult result =  bndles.get(i);
                            TriggerFiredBundle bndle =  result.getTriggerFiredBundle();
                            Exception exception = result.getException();

                            if (exception instanceof RuntimeException) {
                                getLog().error("RuntimeException while firing trigger " + triggers.get(i), exception);
                                qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                                continue;
                            }

                            // it's possible to get 'null' if the triggers was paused,
                            // blocked, or other similar occurrences that prevent it being
                            // fired at this time...  or if the scheduler was shutdown (halted)
                            if (bndle == null) {
                                qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                                continue;
                            }
							//zhix
                            JobRunShell shell = null;
                            try {
                            //执行任务,详细在 JobRunShell 的run方法中
                                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;
                            }

                            if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
                                // this case should never happen, as it is indicative of the
                                // scheduler being shutdown or a bug in the thread pool or
                                // a thread pool being used concurrently - which the docs
                                // say not to do...
                                getLog().error("ThreadPool.runInThread() return false!");
                                qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
                            }

                        }

                        continue; // while (!halted)
                    }
                } else { // if(availThreadCount > 0)
                    // should never happen, if threadPool.blockForAvailableThreads() follows contract
                    continue; // while (!halted)
                }
			//省略部分代码
             
        } // while (!halted)
    }

3.1、 acquireNextTriggers方法

acquireNextTriggers方法意为获取将要执行的触发器。

  • idleWaitTime 默认30秒,意为获取将来30秒要执行的触发器。
  • availThreadCount空闲线程数量。
  • getMaxBatchSize 每一次拉取触发器的数量,默认为1。
  • BatchTimeWindow 时间窗口参数,默认为0。
    这样,在此即可简单认为,获取一个将来30秒内要触发的触发器。

3.1.1、JobStoreSupport.acquireNextTriggers

public List<OperableTrigger> acquireNextTriggers(final long noLaterThan, final int maxCount, final long timeWindow)
        throws JobPersistenceException {
        //默认maxCount为1,因此默认的时候是不会使用悲观锁。
        // 那为何集群模式默认不使用锁呢?这里稍后解释用处。
        String lockName;
        if(isAcquireTriggersWithinLock() || maxCount > 1) { 
            lockName = LOCK_TRIGGER_ACCESS;
        } else {
            lockName = null;
        }
        return executeInNonManagedTXLock(lockName, 
                new TransactionCallback<List<OperableTrigger>>() {
                    public List<OperableTrigger> execute(Connection conn) throws JobPersistenceException {
                        return acquireNextTrigger(conn, noLaterThan, maxCount, timeWindow);
                    }
                },
                new TransactionValidator<List<OperableTrigger>>() {
                    public Boolean validate(Connection conn, List<OperableTrigger> result) throws JobPersistenceException {
                        try {
                            List<FiredTriggerRecord> acquired = getDelegate().selectInstancesFiredTriggerRecords(conn, getInstanceId());
                            Set<String> fireInstanceIds = new HashSet<String>();
                            for (FiredTriggerRecord ft : acquired) {
                                fireInstanceIds.add(ft.getFireInstanceId());
                            }
                            for (OperableTrigger tr : result) {
                                if (fireInstanceIds.contains(tr.getFireInstanceId())) {
                                    return true;
                                }
                            }
                            return false;
                        } catch (SQLException e) {
                            throw new JobPersistenceException("error validating trigger acquisition", e);
                        }
                    }
                });
    }

3.1.2、 acquireNextTrigger获取触发器

protected List<OperableTrigger> acquireNextTrigger(Connection conn, long noLaterThan, int maxCount, long timeWindow){
   // 中间省略部分代码
  //1.
   List<TriggerKey> keys = getDelegate().selectTriggerToAcquire(conn, noLaterThan + timeWindow, getMisfireTime(), maxCount);
  }
  // 中间省略部分代码
  //2.
   JobKey jobKey = nextTrigger.getJobKey();
   JobDetail job;
      try {
          job = retrieveJob(conn, jobKey);
      } catch (JobPersistenceException jpe) {
          try {
              getLog().error("Error retrieving job, setting trigger state to ERROR.", jpe);
              getDelegate().updateTriggerState(conn, triggerKey, STATE_ERROR);
          } catch (SQLException sqle) {
              getLog().error("Unable to set trigger state to ERROR.", sqle);
          }
          continue;
      }
	 //中间省略部分代码
	 //3.
 	 int rowsUpdated = getDelegate().updateTriggerStateFromOtherState(conn, triggerKey, STATE_ACQUIRED, STATE_WAITING);
     if (rowsUpdated <= 0) {
            continue; // next trigger
        }
     //中间省略部分代码
      //4. 
     getDelegate().insertFiredTrigger(conn, nextTrigger, STATE_ACQUIRED, null);

获取的过程很麻烦步骤如下。

  1. 获取qrtz_triggers表状态为WAITING的触发器信息。order by NEXT_FIRE_TIME ASC ,PRIORITY DESC按照这个规则进行排序。
  2. 验证查询回来的信息是否合法,查询qrtz_job_details表是否存在job信息。
  3. 如果triggers合法,则讲这条记录修改为ACQUIRED状态。这里的修改通过乐观锁的思想进行修改。通过StdJDBCConstants.UPDATE_TRIGGER_STATE_FROM_STATE查看。
  4. 修改完成,则证明当前调度器可以对此作业进行调度。则插入qrtz_fired_triggers表中一条正在执行此作业的记录。

3.2、 qsRsrcs.getJobStore().triggersFired

插入qrtz_fired_triggers的记录开始是为ACQUIRED状态,这里修改为执行中EXECUTING

return executeInNonManagedTXLock(LOCK_TRIGGER_ACCESS,
                new TransactionCallback<List<TriggerFiredResult>>() {
                    public List<TriggerFiredResult> execute(Connection conn) throws JobPersistenceException {
                        List<TriggerFiredResult> results = new ArrayList<TriggerFiredResult>();

                        TriggerFiredResult result;
                        for (OperableTrigger trigger : triggers) {
                            try {
                              TriggerFiredBundle bundle = triggerFired(conn, trigger);
                              result = new TriggerFiredResult(bundle);
                            } catch (JobPersistenceException jpe) {
                                result = new TriggerFiredResult(jpe);
                            } catch(RuntimeException re) {
                                result = new TriggerFiredResult(re);
                            }
                            results.add(result);
                        }

                        return results;
                    }
                }
                //省略部分代码

思路:

  1. 查询qrtz_triggers表是否是ACQUIRED状态。
  2. 验证qrtz_job_details表的job是否合法。
  3. 修改qrtz_fired_triggers表为EXECUTING状态。
    这个执行的过程全程是加锁的。到这时修改成功的触发器即可在当前调度器进行调度。

3.3、JobRunShell.run任务调度

任务调度的源码在JobRunShell.run方法中。
在此方法中,通过job.execute方法调度任务。jobExEx记录各种异常信息,最后通过qs.notifyJobStoreJobComplete(trigger, jobDetail, instCode);修改qrtz_triggers表状态,和删除qrtz_fired_triggers表的信息。

附录

悲观锁与乐观锁
quartz框架中有多次通过qrtz_locks表的数据使用悲观锁来保证线程安全。数据如下,
在这里插入图片描述

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

sql中通过for update 关键字来实现悲观锁。
       悲观锁是很消耗性能的,但是这里却这么用了,肯定有必须要用的道理。思考至今得出,乐观锁适用于update语句之后做一些事物处理,主要通过update语句来过滤出只有一个线程能继续向下进行。悲观锁可以处理select语句之后的insert语句重复插入的问题,而且在几条数据状态频繁变更的时候,悲观锁也可以防止死锁的产生。
       在第二步的时候也有插入操作,这个插入操作并没有在update语句之后,通过悲观锁可以防止重复插入。
在这里插入图片描述

       第三步事务的代码中有众多状态变更,有可能出现死锁情况。如下图:
在这里插入图片描述
相信我,这两个都只是我的猜想,可能都是错的,如果您有更好的见解,请告诉我。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叁滴水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值