Quartz source code analysis - thread scheduling

Disclaimer: This article is a blogger original article, welcome to reprint. https://blog.csdn.net/guo_xl/article/details/85165333

purpose

In springboot-Quartz integration and demo source code analysis , the demand is distributed clustered environment:

  • Which can only be acquired in which one machine to lock, while other machines can not get to.
  • But hang the machine, other machines to be able to take over the

In the very beginning to consider scheduling functions, there is related to this function, it was like they used to implement distributed database lock. Quartz was later found to have been achieved, the spirit know these know why, how research is done under the Quartz.

Quartz threading model

  • ThreadExecutor schedule threads
  • ThreadPool (SimpleThreadPool) pool of worker threads

Quartz table

Table name Description
QRTZ_JOB_DETAILS Table of specific work
QRTZ_TRIGGERS Record trigger table by TRIGGER_TYPE to distinguish what kind of trigger, including SIMPLE, CRON, DAILY_I, CAL_INT, BLOB. It corresponds SimpleTrigger, CronTirgger, DateIntervalTrigger, and NthIncludedDayTrigger
QRTZ_SIMPLE_TRIGGERS SIMPLE type of trigger table, only the interval of a trigger field
QRTZ_BLOB_TRIGGERS BLOBL type of trigger table
QRTZ_CRON_TRIGGERS CRON type of trigger table
QRTZ_FIRED_TRIGGERS Storing state information related to Trigger activated, and the execution information associated Job
QRTZ_CALENDARS Calendar information is stored Quartz
QRTZ_LOCKS A row lock table, QRTZ_LOCKS is Quartz cluster synchronization mechanisms row lock table
QRTZ_SCHEDULER_STATE Recording scheduler instance object
StdSchedulerFactory start

In springboot-Quartz integrated source code analysis and demo
in the final analysis drawn Quartz start StdSchedulerFactory, will be called to
StdSchedulerFactory#private Scheduler instantiate() throws SchedulerException {}
in instantiate()a few of the more important properties in the initialization,
in addition to JobStore, there ThreadPool and ThreadExecutor

private Scheduler instantiate() throws SchedulerException {
        JobStore js = null;
        ThreadPool tp = null;
        ThreadExecutor threadExecutor;
}
ThreadPool

Quartz start ThreadPool (SimpleThreadPool) pool of worker threads

private Scheduler instantiate() throws SchedulerException {
        ThreadPool tp = null;
        //默认是SimpleThreadPool
        String tpClass = cfg.getStringProperty(PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName());

        if (tpClass == null) {
            initException = new SchedulerException(
                    "ThreadPool class not specified. ");
            throw initException;
        }

        try {
            tp = (ThreadPool) loadHelper.loadClass(tpClass).newInstance();
        } catch (Exception e) {
            ...
        }
        tProps = cfg.getPropertyGroup(PROP_THREAD_POOL_PREFIX, true);
        try {
            setBeanProps(tp, tProps);
        } catch (Exception e) {
            ...
        }
        ...
        tp.initialize();//启动threadpool
        ...
}

The default configuration in some of the properties SimpleThreadPool quartz.jar of
Quartz.propertis in, for example,

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

tp.initialize() 即SimpleThreadPool#initialize()

public class SimpleThreadPool implements ThreadPool {
 private List<WorkerThread> workers;
    private LinkedList<WorkerThread> availWorkers = new LinkedList<WorkerThread>();
    private LinkedList<WorkerThread> busyWorkers = new LinkedList<WorkerThread>();
    
    public void initialize() throws SchedulerConfigException {
        ...
        // 创建worker线程数
        Iterator<WorkerThread> workerThreads = createWorkerThreads(count).iterator();
        // 循环启动
        while(workerThreads.hasNext()) {
            WorkerThread wt = workerThreads.next();
            wt.start();
            //availWorkers是空闲的线程,一开始肯定都是空闲的
            availWorkers.add(wt);
        }
    }

}
WorkerThread
class WorkerThread extends Thread {
        //AtomicBoolean来标识是否线程已经停止了
private AtomicBoolean run = new AtomicBoolean(true);
public void run(Runnable newRunnable) {
            synchronized(lock) {
                if(runnable != null) {
                    throw new IllegalStateException("Already running a Runnable!");
                }
                runnable = newRunnable;
                lock.notifyAll();
            }
 }
public void run() {
            boolean ran = false;
            while (run.get()) {
                try {
                    synchronized(lock) {
                        while (runnable == null && run.get()) {
                            lock.wait(500);
                        } 
                        //启动传入的runnable的run(),不是start()
                        if (runnable != null) {
                            ran = true;
                            runnable.run();
                        }
                    }
                } catch (InterruptedException unblock) {
                ...
                } catch (Throwable exceptionInRunnable) {
                ...
                } finally {
                    if (runOnce) {
                           run.set(false);                        clearFromBusyWorkersList(this);
                    } else if(ran) {
                        ran = false;
                        makeAvailable(this);
                    }

                }
            }
        }
    }
    
}
ThreadExecutor

ThreadExecutor default is DefaultThreadExecutor, instantiate in

private Scheduler instantiate() throws SchedulerException {
        ThreadExecutor threadExecutor;
        String threadExecutorClass = cfg.getStringProperty(PROP_THREAD_EXECUTOR_CLASS);
        if (threadExecutorClass != null) {
            tProps = cfg.getPropertyGroup(PROP_THREAD_EXECUTOR, true);
            try {
                threadExecutor = (ThreadExecutor) loadHelper.loadClass(threadExecutorClass).newInstance();
                log.info("Using custom implementation for ThreadExecutor: " + threadExecutorClass);

                setBeanProps(threadExecutor, tProps);
            } catch (Exception e) {
                initException = new SchedulerException(
                        "ThreadExecutor class '" + threadExecutorClass + "' could not be instantiated.", e);
                throw initException;
            }
        } else {
            log.info("Using default implementation for ThreadExecutor");
            //使用默认的DefaultThreadExecutor
            threadExecutor = new DefaultThreadExecutor();
        }
        QuartzSchedulerResources rsrcs = new QuartzSchedulerResources();
        rsrcs.setThreadExecutor(threadExecutor);
        qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);
}

See QuartzScheduler (rsrcs, idleWaitTime, dbFailureRetry) constructor

public QuartzScheduler(QuartzSchedulerResources resources, long idleWaitTime, @Deprecated long dbRetryInterval)
        throws SchedulerException {
        ...
        this.schedThread = new QuartzSchedulerThread(this, resources);
        //schedThreadExecutor可以由
        //org.quartz.threadExecutor.class来指定定义,如果没有定义默认是DefaultThreadExecutor
        ThreadExecutor schedThreadExecutor = resources.getThreadExecutor();
        //启动QuartzSchedulerThread
        schedThreadExecutor.execute(this.schedThread);
        ...
    }
public class DefaultThreadExecutor implements ThreadExecutor {

    public void initialize() {
    }

    public void execute(Thread thread) {
        thread.start();
    }

}

QuartzSchedulerThread extends Thread, mainly to see
QuartzSchedulerThread # run () method, DefaultThreadExecutor # execute execution is
QuartzSchedulerThread the run ()

 public void run() {
        ...
        //返回0之前会一直阻塞
        int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
                if(availThreadCount > 0) {//一定是true

                    List<OperableTrigger> triggers;

                    long now = System.currentTimeMillis();

                    clearSignaledSchedulingChange();
                    try {
                    //获取到在一定空闲时间内的任务
                        triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                                now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
        ...
                    List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
                                            if(res != null)
                                                bndles = res;
 }

qsRsrcs.getJobStore().acquireNextTriggers()

qsRsrcs.getJobStore()=JobStoreSupport

 public List<OperableTrigger> acquireNextTriggers(final long noLaterThan, final int maxCount, final long timeWindow)
        throws JobPersistenceException {
        
        String lockName;
        if(isAcquireTriggersWithinLock() || maxCount > 1) { 
            lockName = LOCK_TRIGGER_ACCESS;
        } else {
            lockName = null;
        }
     //这个方法要先获取到锁,使用非管理的事务,所谓非管理,就是说事务需要代码手动提交,下面注释是源码里的注释
 /**
     * Execute the given callback having optionally acquired the given lock.
     * This uses the non-managed transaction connection.
     * 
     *
     * @param lockName The name of the lock to acquire, for example
     * "TRIGGER_ACCESS".  If null, then no lock is acquired, but the
     * lockCallback is still executed in a non-managed transaction. 
     */
        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);
                        }
                    }
                });
    }

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.
                //可以不适用数据库做分布式锁,
                //默认是使用的,getLockHandler()返回的是org.quartz.impl.jdbcjobstore.StdRowLockSemaphore
                if (getLockHandler().requiresConnection()) {
                    conn = getNonManagedTXConnection();
                }
                
                //使用数据库来获取到锁,
                //锁使用的是SELECT * FROM QRTZ_LOCKS WHERE SCHED_NAME = 'quartzScheduler' AND LOCK_NAME = 'TRIGGER_ACCESS' FOR UPDATE来获取到 的行锁
                transOwner = getLockHandler().obtainLock(conn, lockName);
            }
            
            if (conn == null) {
                conn = getNonManagedTXConnection();
            }
            
            //获取到锁后,其它的线程或进程都会阻塞在这里。而获取到锁的线/程会继续执行任务
            final T result = txCallback.execute(conn);
            
            //完后需要手动提交,
            try {
                commitConnection(conn);
            } catch (JobPersistenceException 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);
            }
        }
    }

transOwner = getLockHandler().obtainLock(conn, lockName);

getLockHandler () is org.quartz.impl.jdbcjobstore.StdRowLockSemaphore

StdRowLockSemaphore.obtainLock(conn, lockName)

 public boolean obtainLock(Connection conn, String lockName)
        throws LockException {

//如果所已经是自己的,不需要再去获取数据库锁
        if (!isLockOwner(lockName)) {

            //!!!关键的获取锁的方法    
            executeSQL(conn, lockName, expandedSQL, expandedInsertSQL);
            
            if(log.isDebugEnabled()) {
                log.debug(
                    "Lock '" + lockName + "' given to: "
                            + Thread.currentThread().getName());
            }
            getThreadLocks().add(lockName);
            
        } else if(log.isDebugEnabled()) {
            log.debug(
                "Lock '" + lockName + "' Is already owned by: "
                        + Thread.currentThread().getName());
        }

        return true;
    }

executeSQL (conn, lockName, expandedSQL, expandedInsertSQL) by acquiring the lock to do, learn more about knowledge in MySQL InnoDB engine of row locks are as follows:

MySQL中InnoDB引擎的行锁是通过加在什么上完成(或称实现)的?为什么是这样子的?
答:InnoDB是基于索引来完成行锁
例: select * from tab_with_index where id = 1 for update;
for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,
如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起。

To get the code acquireNextTrigger (conn, noLaterThan, maxCount, timeWindow) after the lock; this is to get to Trigger a collection of all executed, these collections naturally to let the ThreadPool in WorkerTheard to deal

protected List<OperableTrigger> acquireNextTrigger(Connection conn, long noLaterThan, int maxCount, long timeWindow)
        throws JobPersistenceException {
        if (timeWindow < 0) {
          throw new IllegalArgumentException();
        }
        
        List<OperableTrigger> acquiredTriggers = new ArrayList<OperableTrigger>();
        Set<JobKey> acquiredJobKeysForNoConcurrentExec = new HashSet<JobKey>();
        final int MAX_DO_LOOP_RETRY = 3;
        int currentLoopCount = 0;
        do {
            currentLoopCount ++;
            try {
                //获取到一定时间段内的Trigger,时间段是可以配置的
                List<TriggerKey> keys = getDelegate().selectTriggerToAcquire(conn, noLaterThan + timeWindow, getMisfireTime(), maxCount);
                
                // No trigger is ready to fire yet.
                if (keys == null || keys.size() == 0)
                    return acquiredTriggers;

                long batchEnd = noLaterThan;

                for(TriggerKey triggerKey: keys) {
                    //再查一次封装成OperableTrigger,为什么不在getDelegate().selectTriggerToAcquire(conn, noLaterThan + timeWindow, getMisfireTime(), maxCount)里全部查出来?
                    OperableTrigger nextTrigger = retrieveTrigger(conn, triggerKey);
                    if(nextTrigger == null) {
                        continue; // next trigger
                    }

                    JobKey jobKey = nextTrigger.getJobKey();
                    JobDetail job;
                    try {
                        //根据trigger里对应的jobKey获取到jobDetail表里对应的job
                        job = retrieveJob(conn, jobKey);
                    } catch (JobPersistenceException jpe) {
                       ...
                        continue;
                    }
                    
                      ...
                    //更新QRTZ_TRIGGERS里的状态,从STATE_WAITING改成STATE_ACQUIRED
                    int rowsUpdated = getDelegate().updateTriggerStateFromOtherState(conn, triggerKey, STATE_ACQUIRED, STATE_WAITING);
                  ...
                    nextTrigger.setFireInstanceId(getFiredTriggerRecordId());
                    //插入一条即将触发的Trigger到QRTZ_FIRED_TRIGGERS表里
                    getDelegate().insertFiredTrigger(conn, nextTrigger, STATE_ACQUIRED, null);
                 ...
                    acquiredTriggers.add(nextTrigger);
                }
              ...
                // We are done with the while loop.
                break;
            } catch (Exception e) {
               ...
            }
        } while (true);
        
        // Return the acquired trigger list
        return acquiredTriggers;
    }
How long trigger section will be to get

Thread dispatch table to qrtz_triggersobtain NEXT_FIRE_TIME <trigger all the current time + 30s + m, sorted by the trigger event, the first n taken. m and n respectively
can be arranged

  • org.quartz.scheduler.batchTriggerAcquisitionMaxCount=n
  • org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow=m
sql analysis

Local using Mysql database, use the following to open sql record

SET GLOBAL log_output = "FILE";
SET GLOBAL general_log_file = "C:/logs/query.log";
SET GLOBAL general_log = 'ON';

The following categories recorded sql

-- 获取锁

SELECT * FROM QRTZ_LOCKS WHERE SCHED_NAME = 'quartzScheduler' AND LOCK_NAME = 'TRIGGER_ACCESS'
for update;

-- 获取得到一定时间内的trigger
SELECT TRIGGER_NAME, TRIGGER_GROUP, NEXT_FIRE_TIME, PRIORITY FROM QRTZ_TRIGGERS WHERE SCHED_NAME = 'quartzScheduler' AND TRIGGER_STATE = 'WAITING' AND NEXT_FIRE_TIME <= 1545293594800 AND (MISFIRE_INSTR = -1 OR (MISFIRE_INSTR != -1 AND NEXT_FIRE_TIME >= 1545293504801)) ORDER BY NEXT_FIRE_TIME ASC, PRIORITY DESC

-- 上面如果有获取到TRIGGER_NAME, TRIGGER_GROUP,则使用下面的sql获取到QRTZ_TRIGGERS
SELECT * FROM QRTZ_TRIGGERS WHERE SCHED_NAME = 'quartzScheduler' AND TRIGGER_NAME = 'trigger-job2' AND TRIGGER_GROUP = 'group1'

SELECT * FROM QRTZ_SIMPLE_TRIGGERS WHERE SCHED_NAME = 'quartzScheduler' AND TRIGGER_NAME = 'trigger-job2' AND TRIGGER_GROUP = 'group1'

SELECT * FROM QRTZ_JOB_DETAILS WHERE SCHED_NAME = 'quartzScheduler' AND JOB_NAME = 'job2' AND JOB_GROUP = 'group1'

-- 更新QRTZ_TRIGGERS里的trigger的状态
UPDATE QRTZ_TRIGGERS SET TRIGGER_STATE = 'ACQUIRED' WHERE SCHED_NAME = 'quartzScheduler' AND TRIGGER_NAME = 'trigger-job2' AND TRIGGER_GROUP = 'group1' AND TRIGGER_STATE = 'WAITING'

-- 插入QRTZ_FIRED_TRIGGERS的待触发状态
INSERT INTO QRTZ_FIRED_TRIGGERS (SCHED_NAME, ENTRY_ID, TRIGGER_NAME, TRIGGER_GROUP, INSTANCE_NAME, FIRED_TIME, SCHED_TIME, STATE, JOB_NAME, JOB_GROUP, IS_NONCONCURRENT, REQUESTS_RECOVERY, PRIORITY) VALUES('quartzScheduler', 'DESKTOP-TTESTAQ15452876685211545287681141', 'trigger-job2', 'group1', 'DESKTOP-TTESTAQ1545287668521', 1545293564804, 1545293566392, 'ACQUIRED', null, null, 0, 0, 5)

-- connection.commit()

in conclusion

  1. Cluster, each machine will have a Scheduler,
    Scheduler there is a thread scheduling, dispatching thread will go to the table qrtz_triggersto obtain NEXT_FIRE_TIME <current time + 30s + all trigger m, sorted by triggering events, taken before the n. m and n respectively
    can be arranged
  • org.quartz.scheduler.batchTriggerAcquisitionMaxCount=n
  • org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow=m
  1. Use for update to get the row locks
  2. UPDATE QRTZ_TRIGGERS from state WAITINGtoACQUIRED
  3. INERT INTO QRTZ_FIRED_TRIGGERSwritten already FIRED of TRIGGER
  4. connection.commit () to release the lock
  5. Remove the trigger will be forwarded to handle the threadpool in workerThread

Guess you like

Origin blog.csdn.net/guo_xl/article/details/85165333