Quartz集群原理分析

一、Quartz概念

Quartz是一个优秀的任务调度框架, 具有以下特点:
  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. 负载均衡
  3. 高可用

调度器:scheduler

任务调度的控制器,负责定时任务的调度,并且提供任务和触发器的增删改查等api方法。

任务:job

job是实际被调度的任务,每个任务必须指定具体执行任务的实现类,实现类需要继承QuartzJobBean或者实现org.quartz.Job接口,具体的业务逻辑写在execute方法里面。
是否支持并发的注解:@DisallowConcurrentExecution

触发器:trigger

trigger用来定义调度时间的概念,即按什么样时间规则去触发任务。主要几种类型:
  • SimpleTrigger:简单触发器,从某个时间开始,每隔多少时间触发,重复多少次。
  • CronTrigger:使用cron表达式定义触发的时间规则,如"0 0 0,2,4 1/1 * ? *" 表示每天的0,2,4点触发。
  • DailyTimeIntervalTrigger:每天中的一个时间段,每N个时间单元触发,时间单元可以是毫秒,秒,分,小时
  • CalendarIntervalTrigger:每N个时间单元触发,时间单元可以是毫秒,秒,分,小时,日,月,年。
trigger状态:WAITING,ACQUIRED,EXECUTING,COMPLETE,BLOCKED,ERROR,PAUSED,PAUSED_BLOCKED,DELETED

未正常触发的任务:misfire job

没有在正常触发时间点触发的任务。主要由一下几种情况导致:
  1. 触发时间在应用不可用的时间内,比如重启
  2. 上次的执行时间过长,超过了下次触发的时间
  3. 任务被暂停一段时间后,重新被调度的时间在下次触发时间之后
处理misfire job的策略,需要在创建trigger的时候配置,每种trigger对应的枚举值都不同,具体在接口里面有定义。CronTrigger有2种处理misfire的策略:
处理策略
描述
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
立即触发一次
MISFIRE_INSTRUCTION_DO_NOTHING
忽略,不处理,等待下次触发

之间的关系:

  • scheduler由工厂类SchedulerFactory创建,主要负责job和trigger的持久化管理,包括新增、删除、修改、触发、暂停、恢复调度、停止调度等;
  • 一个job可以关联多个trigger,但是一个trigger只能关联一个job。

二、集群模式

Quartz的集群模式指的是一个集群下多个节点管理同一批任务的调度,通过共享数据库的方式实现,保证同一个任务到达触发时间的时候,只有一台机器去执行该任务。每个节点部署一个单独的quartz实例,相互之间没有直接数据通信。

spring配置

<bean id="quartzJobFactory" class="com.taobao.trip.hcs.service.gubei.job.AutowiringSpringBeanJobFactory">
    <property name="ignoredUnknownProperties" value="applicationContext"/>
bean>


<bean id="quartzScheduler"
      class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="jobFactory" ref="quartzJobFactory"/>
    
    <property name="dataSource" ref="dataSource" />
    
    <property name="quartzProperties">
        <props>
            <prop key="org.quartz.scheduler.instanceName">${quartz.scheduler.instanceName}}prop>
            <prop key="org.quartz.scheduler.instanceId">hcs-crawlprop>
            
            <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPoolprop>
            <prop key="org.quartz.threadPool.threadCount">20prop>
            <prop key="org.quartz.threadPool.threadPriority">5prop>
            
            
            
            <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTXprop>
            <prop key="org.quartz.jobStore.misfireThreshold">120000prop>

            <prop key="org.quartz.jobStore.tablePrefix">QRTZ_prop>
        props>

    property>
    
    <property name="schedulerName" value="${quartz.scheduler.schedulerName}" />
    
    <property name="startupDelay" value="30" />

    <property name="applicationContextSchedulerContextKey" value="applicationContextKey" />

    <property name="overwriteExistingJobs" value="true" />

    <property name="autoStartup" value="true" />

    <property name="triggers">
        <list>
            <ref bean="cronTrigger" />
            <ref bean="simpleTrigger" />
        list>
    property>

    <property name="jobDetails">
        <list>
            <ref bean="jobDetail" />
        list>
    property>
bean>

<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
     <property name="name" value="testJob" />
     <property name="jobClass" value="com.taobao.trip.hcs.service.common.UniversalActionJob"/>
     
bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
    <property name="jobDetail" ref="jobDetail1" />
    <property name="cronExpression" value="0/10 * * * * ?" />
bean>

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
    <property name="repeatInterval" value="30000" />
    <property name="repeatCount" value="1000" />
    <property name="jobDetail" ref="jobDetail1" />
bean>
AutowiringSpringBeanJobFactory:把job添加到spring容器里面
public class AutowiringSpringBeanJobFactory
        extends SpringBeanJobFactory implements ApplicationContextAware {
    private transient AutowireCapableBeanFactory beanFactory;

    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    public Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);  //the magic is done here
        return job;
    }
}

三、集群模式原理分析

数据库表:

主要的几个表
表名
描述
QRTZ_CRON_TRIGGERS
存储CronTrigger,包括Cron表达式和时区信息
QRTZ_FIRED_TRIGGERS
存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
QRTZ_PAUSED_TRIGGER_GRPS
存储已暂停的Trigger组的信息
QRTZ_JOB_DETAILS
存储每一个已配置的Job的详细信息
QRTZ_SIMPLE_TRIGGERS
存储简单的Trigger,包括重复次数、间隔、以及已触的次数
QRTZ_TRIGGERS
存储所有的Trigger的详细信息:job名称,trigger类型、状态、下次触发时间
QRTZ_LOCKS
存储行锁的表
QRTZ_LOCKS就是Quartz集群实现同步机制的行锁表,例如htc_scheduler集群下的锁:

线程

负责任务调度的几个线程:
(1)任务执行线程:通常使用一个线程池(SimpleThreadPool)维护一组线程,负责实际每个job的执行。
(2)Scheduler调度线程QuartzSchedulerThread :轮询存储的所有 trigger,如果有需要触发的 trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该 trigger 关联的任务。
(3)处理misfire job的线程MisfireHandler:轮训所有misfire的trigger,原理就是从数据库中查询所有下次触发时间小于当前时间的trigger,按照每个trigger设定的misfire策略处理这些trigger。

源码分析

通过源码看下集群模式下如何处理trigger,主线程QuartzSchedulerThread run()方法:
@Override
public void run() {
    boolean lastAcquireFailed = false;

    while (!halted.get()) {
        int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
        if(availThreadCount > 0) {
            ...
            // 获取时间idleWaitTime(默认30s)内一定数量的trigger,距下次触发时间不超过TimeWindow(默认0)
            // 同时修改trigger的状态,从WAITING更新为ACQUIRED
            triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                                now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
            ...
            // 这里修改trigger的状态,从ACQUIRED改为EXECUTING,更新trigger下次触发时间
            // 再把状态改为WAITING或者BLOCKED或者COMPLETE
            List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
            ...
            // 创建具体执行任务的JobRunShell
            shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
            ...
            // 提交给线程池执行任务
            qsRsrcs.getThreadPool().runInThread(shell)
        }
    } // while (!halted)

    // drop references to scheduler stuff to aid garbage collection...
    qs = null;
    qsRsrcs = null;
}
JobRunShell:
public void run() {
    ...
    // 从上下文获取任务
    Job job = jec.getJobInstance();
    ...
    // 任务实际执行
    job.execute(jec);
    ...
    // 执行结束,修改trigger状态
    qs.notifyJobStoreJobComplete(trigger, jobDetail, instCode);
}
所以,主线程QuartzSchedulerThread 就是不断查询需要触发的trigger,获取trigger,执行trigger关联的任务,释放trigger。
qsRsrcs.getJobStore()对应的处理类是JobStore,对应的就是在配置文件中配置的实现类:



<prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTXprop>
大部分对任务和trigger的操作在JobStoreTX的父类JobStoreSupport中实现的,其中acquireNextTrigger,triggerFired,releaseAcquiredTrigger都需要先获取TRIGGER_ACCESS,才能执行对应的DB操作。主要的逻辑在executeInNonManagedTXLock方法里面:
protected <T> T executeInNonManagedTXLock(
        String lockName, 
        TransactionCallback<T> txCallback, final TransactionValidator<T> txValidator) throws JobPersistenceException {
    boolean transOwner = false;
    Connection conn = null;
    try {
        if (lockName != null) {
            // If we aren't using db locks, then delay getting DB connection 
            // until after acquiring the lock since it isn't needed.
            if (getLockHandler().requiresConnection()) {
                conn = getNonManagedTXConnection();
            }
            
            transOwner = getLockHandler().obtainLock(conn, lockName);
        }
        
        if (conn == null) {
            conn = getNonManagedTXConnection();
        }
        
        final T result = txCallback.execute(conn);
        try {
            commitConnection(conn);
        } catch (JobPersistenceException e) {
            rollbackConnection(conn);
            if (txValidator == null || !retryExecuteInNonManagedTXLock(lockName, new TransactionCallback<Boolean>() {
                @Override
                public Boolean execute(Connection conn) throws JobPersistenceException {
                    return txValidator.validate(conn, result);
                }
            })) {
                throw e;
            }
        }

        Long sigTime = clearAndGetSignalSchedulingChangeOnTxCompletion();
        if(sigTime != null && sigTime >= 0) {
            signalSchedulingChangeImmediately(sigTime);
        }
        
        return result;
    } catch (JobPersistenceException e) {
        rollbackConnection(conn);
        throw e;
    } catch (RuntimeException e) {
        rollbackConnection(conn);
        throw new JobPersistenceException("Unexpected runtime exception: "
                + e.getMessage(), e);
    } finally {
        try {
            releaseLock(lockName, transOwner);
        } finally {
            cleanupConnection(conn);
        }
    }
}
获取锁、释放锁的操作在StdRowLockSemaphore里面完成:
public boolean obtainLock(Connection conn, String lockName)
    throws LockException {

    if(log.isDebugEnabled()) {
        log.debug(
            "Lock '" + lockName + "' is desired by: "
                    + Thread.currentThread().getName());
    }
    if (!isLockOwner(lockName)) {

        executeSQL(conn, lockName, expandedSQL, expandedInsertSQL);
        
        if(log.isDebugEnabled()) {
            log.debug(
                "Lock '" + lockName + "' given to: "
                        + Thread.currentThread().getName());
        }
        getThreadLocks().add(lockName);
        //getThreadLocksObtainer().put(lockName, new
        // Exception("Obtainer..."));
    } else if(log.isDebugEnabled()) {
        log.debug(
            "Lock '" + lockName + "' Is already owned by: "
                    + Thread.currentThread().getName());
    }

    return true;
}
具体执行的sql如:
SELECT * FROM t WHERE SCHED_NAME = 'SCHED_NAME' AND LOCK_NAME = 'TRIGGER_ACCESS' FOR UPDATE;
执行成功后,数据库对该行ROW LOCK,若此时,另外一个线程使用相同的SQL对表的数据进行查询,由于查询出的数据行已经被数据库锁住了,此时这个线程就只能等待,直到拥有该行锁的线程完成了相关的业务操作,执行了commit动作后,数据库才会释放了相关行的锁,这个线程才能继续执行。这样就保证了同一个集群下,只有一个quartz实例获取需要执行的trigger。


四、参考资料

1. http://www.quartz-scheduler.org/
2. https://tech.meituan.com/mt-crm-quartz.html
3. https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/
4. https://blog.csdn.net/a67474506/article/details/38402059

你可能感兴趣的:(中间件)