Quartz 2.2 的实现原理和运行过程

一、Quartz 的几个概念类

这几个概念类,是我们调用Quartz任务调度的基础。了解清楚之后,我们再来看一下如何去启动和关闭一个Quartz调度程序。

  • 1、org.quartz.Job 
    它是一个抽象接口,表示一个工作,也就是我们要执行的具体内容,他只定义了一个几口方法: 
    void execute(JobExecutionContext context) 
    作用等同Spring的: 
    org.springframework.scheduling.quartz.QuartzJobBean
  • 2、org.quartz.JobDetail 
    JobDetail表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容,它包含了这个任务调度的方案和策略。 
    他的实现类: 
    org.quartz.impl.JobDetailImpl 
    作用等同Spring: 
    org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean
  • 3、org.quartz.Trigger 
    它是一个抽象接口,表示一个调度参数的配置,通过配置它,来告诉调度容器什么时候去调用JobDetail。 
    他的两个实现类: 
    org.quartz.impl.triggers.SimpleTriggerImpl 
    org.quartz.impl.triggers.CronTriggerImpl 
    等同于Spring的: 
    org.springframework.scheduling.quartz.SimpleTriggerBean 
    org.springframework.scheduling.quartz.CronTriggerBean 
    前者只支持按照一定频度调用任务,如每隔30分钟运行一次。 
    后者既支持按照一定频度调用任务,又支持定时任务。
  • 4、org.quartz.Scheduler 
    代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。它的方法有start()、shutdown()等方法,负责管理整个调度作业。 
    等同Spring的: org.springframework.scheduling.quartz.SchedulerFactoryBean

二、Quartz 入门示例

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class Quartz {

    public static void main(String[] args) {
        // 1、创建JobDetial对象 , 并且设置选项
        JobDetail jobDetail= JobBuilder.newJob(MyJob.class).withIdentity("testJob_1","group_1").build();

        // 2、通过 TriggerBuilder 创建Trigger对象
        TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
        triggerBuilder.withIdentity("trigger_1", "group_1");
        triggerBuilder.startNow();
        // 设置重复次数和间隔时间
        triggerBuilder.withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInMilliseconds(1) //时间间隔
                .withRepeatCount(5) // 重复次数
         );
        // 设置停止时间
        //triggerBuilder.endAt(new Date(System.currentTimeMillis() + 3));
        // 创建Trigger对象
        Trigger trigger = triggerBuilder.build();

        // 3、创建Scheduler对象,并配置JobDetail和Trigger对象
        SchedulerFactory sf = new StdSchedulerFactory();
        try {
            Scheduler scheduler = sf.getScheduler();
            scheduler.scheduleJob(jobDetail, trigger);
            // 4、并执行启动、关闭等操作
            scheduler.start();

          //关闭调度器
          //scheduler.shutdown(true); 
        } catch (SchedulerException e) {
            e.printStackTrace(); 
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class MyJob implements Job{

    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(new Date() + ": doing something...");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

执行结果:

Wed Aug 03 19:12:53 CST 2016: doing something… 
Wed Aug 03 19:12:53 CST 2016: doing something… 
Wed Aug 03 19:12:53 CST 2016: doing something… 
Wed Aug 03 19:12:53 CST 2016: doing something… 
Wed Aug 03 19:12:53 CST 2016: doing something… 
Wed Aug 03 19:12:53 CST 2016: doing something…

当把结束时间改为:

// 设置停止时间
triggerBuilder.endAt(new Date(System.currentTimeMillis() + 3));
  • 1
  • 2

执行结果:

Wed Aug 03 19:17:12 CST 2016: doing something… 
Wed Aug 03 19:17:12 CST 2016: doing something… 
Wed Aug 03 19:17:12 CST 2016: doing something…

结果分析: 
可以看出,设置重复执行5次,没设置停止时间的时候,执行了6次,当停止时间设置为new Date(System.currentTimeMillis() + 3时, 果只执行了3次。 也就是说当到了停止时间,不管执行的次数是否到达最大次数,都不在执行了。 

当添加了关闭调度器:

//关闭调度器
scheduler.shutdown(true); 
  • 1
  • 2

执行结果:

Wed Aug 03 19:17:12 CST 2016: doing something…

三、Quartz 简单总结

  • 1、scheduler.shutdown(true); // true表示等到本次执行结束
  • 2、为了配置强大时间调度策略,可以研究专门的CronTrigger。
  • 3、整合Spring时,Quartz容器关闭方式有两种方法,一种是关闭- Spring容器,一种是获取到SchedulerFactoryBean实例,然后调用一个shutdown就搞定了。
  • 4、Quartz的JobDetail、Trigger都可以在运行时重新设置,并且在下次调用时候起作用。这就为动态作业的实现提供了依据。你可以将调度时间策略存放到数据库,然后通过数据库数据来设定Trigger,这样就能产生动态的调度。
  • 5、JobDetail不存储具体的实例,但它允许你定义一个实例,JobDetail 又指向JobDataMap。 JobDetail持有Job的详细信息,如它所属的组,名称等信息
  • 6、JobDataMap保存着任务实例的对象,并保持着他们状态信息,它是Map接口的实现,即你可以往里面put和get一些你想存储和获取的信息.
  • 7、scheduler容器包含多个JobDetail和Trigger。scheduler是个容器,容器中有一个线程池,用来并行调度执行每个作业,这样可以提高容器效率。

四、Quartz 运行过程

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

0.调度器线程run()

1.获取待触发trigger 
1.1读取JobDetail信息 
1.2读取trigger表中触发器信息并标记为”已获取”

2.触发trigger 
2.1确认trigger的状态 
2.2读取trigger的JobDetail信息 
2.3读取trigger的Calendar信息 
2.4更新trigger信息

3实例化并执行Job 
3.1从线程池获取线程执行JobRunShell的run方法

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

SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sch = sf.getScheduler(); 
// 然后启动:
sch.start();
  • 1
  • 2
  • 3
  • 4

下面跟进StdSchedulerFactory的getScheduler()方法:

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

我们再看一下 sched = instantiate(); 调度器的初始化方法 
- 读取配置资源, 
- 生成QuartzScheduler对象, 
- 创建该对象的运行线程,并启动线程; 
- 初始化JobStore,QuartzScheduler,DBConnectionManager等重要组件, 
- 至此,调度器的初始化工作已完成,初始化工作中quratz读取了数据库中存放的对应当前调度器的锁信息,对应CRM中的表QRTZ2_LOCKS,中的STATE_ACCESS,TRIGGER_ACCESS两个LOCK_NAME.

public void initialize(ClassLoadHelper loadHelper,
            SchedulerSignaler signaler) throws SchedulerConfigException {
        if (dsName == null) {
            throw new SchedulerConfigException("DataSource name not set.");
        }
        classLoadHelper = loadHelper;
        if(isThreadsInheritInitializersClassLoadContext()) {
            log.info("JDBCJobStore threads will inherit ContextClassLoader of thread: " + Thread.currentThread().getName());
            initializersLoader = Thread.currentThread().getContextClassLoader();
        }

        this.schedSignaler = signaler;
        // If the user hasn't specified an explicit lock handler, then
        // choose one based on CMT/Clustered/UseDBLocks.
        if (getLockHandler() == null) {

            // If the user hasn't specified an explicit lock handler,
            // then we *must* use DB locks with clustering
            if (isClustered()) {
                setUseDBLocks(true);
            }

            if (getUseDBLocks()) {
                if(getDriverDelegateClass() != null && getDriverDelegateClass().equals(MSSQLDelegate.class.getName())) {
                    if(getSelectWithLockSQL() == null) {
                        //读取数据库LOCKS表中对应当前调度器的锁信息
                        String msSqlDflt = "SELECT * FROM {0}LOCKS WITH (UPDLOCK,ROWLOCK) WHERE " + COL_SCHEDULER_NAME + " = {1} AND LOCK_NAME = ?";
                        getLog().info("Detected usage of MSSQLDelegate class - defaulting 'selectWithLockSQL' to '" + msSqlDflt + "'.");
                        setSelectWithLockSQL(msSqlDflt);
                    }
                }
                getLog().info("Using db table-based data access locking (synchronization).");
                setLockHandler(new StdRowLockSemaphore(getTablePrefix(), getInstanceName(), getSelectWithLockSQL()));
            } else {
                getLog().info(
                    "Using thread monitor-based data access locking (synchronization).");
                setLockHandler(new SimpleSemaphore());
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

当调用sch.start();方法时, scheduler做了如下工作: 
1.通知listener开始启动 
2.启动调度器线程 
3.启动plugin 
4.通知listener启动完成

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

调度过程

调度器启动后,调度器的线程就处于运行状态了,开始执行quartz的主要工作–调度任务. 前面已介绍过,任务的调度过程大致分为三步: 
1.获取待触发trigger 
2.触发trigger 
3.实例化并执行Job 
而调度过程的三个步骤就承载在QuartzSchedulerThread这个调度器线程类的run()方法中。代码大家可以看一下源码。我在这里就略过性的支出run()方法中对应上面三个步骤的源码。

触发器的获取,run()方法中获取待触发trigger 
调用JobStoreSupport.acquireNextTriggers方法:

 //调度器在trigger队列中寻找30秒内一定数目的trigger准备执行调度,
                        //参数1:nolaterthan = now+3000ms,参数2 最大获取数量,大小取线程池线程剩余量与定义值得较小者
                        //参数3 时间窗口 默认为0,程序会在nolaterthan后加上窗口大小来选择trigger
                        triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                                now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
  • 1
  • 2
  • 3
  • 4
  • 5

参数的意义如下: 
参数1:nolaterthan = now+3000ms,即未来30s内将会被触发. 
参数2 最大获取数量,大小取线程池线程剩余量与定义值得较小者. 
参数3 时间窗口 默认为0,程序会在nolaterthan后加上窗口大小来选择

触发trigger: QuartzSchedulerThread.run() 
调用JobStoreSupport.triggersFired方法:

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

该方法做了以下工作: 
1.获取trigger当前状态 
2.通过trigger中的JobKey读取trigger包含的Job信息 
3.将trigger更新至触发状态 
4.结合calendar的信息触发trigger,涉及多次状态更新 
5.更新数据库中trigger的信息,包括更改状态至STATE_COMPLETE,及计算下一次触发时间. 
6.返回trigger触发结果的数据传输类TriggerFiredBundle

Job执行过程:QuartzSchedulerThread.run()

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

为每个Job生成一个可运行的RunShell,并放入线程池运行. 
在最后调度线程生成了一个随机的等待时间,进入短暂的等待,这使得其他节点的调度器都有机会获取数据库资源.如此就实现了quratz的负载平衡. 
这样一次完整的调度过程就结束了.调度器线程进入下一次循环.

猜你喜欢

转载自blog.csdn.net/qq_36838191/article/details/80479983
2.2