spring quartz cluster construction

Keywords: Java Spring Back-end Quartz

background

quartz can be used to manage and schedule scheduled tasks. There are cluster mode and stand-alone mode. quartz's stand-alone mode is deployed. All task execution information is saved in memory. There is a single point of failure. quartz's cluster mode has the characteristics of high availability and automatic load balancing, which can ensure the execution of scheduled tasks.

1.1 establishment of springboot + MySQL + quartz cluster mode

Note: the cluster mode depends on the time synchronization between the machines where the instance is located. Please deploy the ntp service for time synchronization.
1.1 establishment of quartz related tables

  • Go to the official website to download quartz, Download address , you need to download version 2.2.3 or lower
  • After decompression, execute docs/dbTables/tables_mysql_innodb.sql script creating tables
  • Check whether the following 11 tables exist in db
+--------------------------+
| QRTZ_BLOB_TRIGGERS       |
| QRTZ_CALENDARS           |
| QRTZ_CRON_TRIGGERS       |
| QRTZ_FIRED_TRIGGERS      |
| QRTZ_JOB_DETAILS         |
| QRTZ_LOCKS               |
| QRTZ_PAUSED_TRIGGER_GRPS |
| QRTZ_SCHEDULER_STATE     |
| QRTZ_SIMPLE_TRIGGERS     |
| QRTZ_SIMPROP_TRIGGERS    |
| QRTZ_TRIGGERS            |
+--------------------------+

1.2 introduction of Quartz related packages in maven

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.2.1</version>
        </dependency>

1.3 creating a quartz configuration file

#Default or change your name
org.quartz.scheduler.instanceName=DefaultQuartzScheduler

#============================================================================
# Configure JobStore
#============================================================================
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.dataSource=qzDS
# Turn on cluster mode
org.quartz.jobStore.isClustered=true
# Cluster instance detection interval ms
org.quartz.jobStore.clusterCheckinInterval=5000

# Timeout threshold ms for misfire tasks
org.quartz.jobStore.misfireThreshold=60000
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate


org.quartz.scheduler.instanceId=AUTO
org.quartz.scheduler.rmi.export=false
org.quartz.scheduler.rmi.proxy=false
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false

# Thread pool settings for worker threads
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=5
org.quartz.threadPool.threadPriority=5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true

#============================================================================
# Configure Datasources
#============================================================================
#Configure data sources
org.quartz.dataSource.qzDS.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.qzDS.URL=jdbc:mysql://127.0.0.1:3306/dbName?characterEncoding=utf8&useSSL=true
org.quartz.dataSource.qzDS.user=xxx
org.quartz.dataSource.qzDS.password=xxx
org.quartz.dataSource.qzDS.validationQuery=select 0 from dual

In particular, explain that the parameter org.quartz.jobStore.misfireThreshold = 60000. The misfire task is the task that missed the scheduling trigger time, and the misfireThreshold is the determination condition for determining that the trigger task is misfire. For example, it is stipulated that a Job should be executed at 11:30. If the scheduling is triggered at 11:33 because the instance hangs or the thread pool is busy, the timeout is 3 minutes, The timeout is > 60000ms, so it is determined as misfire.

The processing rules determined as misfire will be mentioned in the following principle introduction and related articles.

1.4 create a job instance factory to solve the spring injection problem. If the default is used, the spring @ Autowired cannot be injected (very important)

@Component
public class MyJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {

    private transient AutowireCapableBeanFactory beanFactory;

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

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
}

1.5 initialization configuration of quartz to generate ScheduleFactory Bean

@Configuration
public class SchedulerConfiguration {

    @Autowired
    private MyJobFactory myJobFactory;

    @Bean(name = "schedulerFactoryBean")
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        //Get configuration properties
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("quartz.properties"));
        //The properties in quartz.properties are read and injected before initializing the object
        propertiesFactoryBean.afterPropertiesSet();
        //Create SchedulerFactoryBean
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        Properties pro = propertiesFactoryBean.getObject();
        factory.setOverwriteExistingJobs(true);
        factory.setAutoStartup(true);
        factory.setQuartzProperties(pro);
        factory.setJobFactory(myJobFactory);
        return factory;
    }

}

1.6 task management implementation class

package com.tencent.oa.fm.digital.ops.intelligent.alarm.server.common.schedules;


import com.alibaba.fastjson.JSONObject;
import com.tencent.oa.fm.digital.ops.intelligent.alarm.contract.SysScheduleTaskDTO;
import com.tencent.oa.fm.digital.ops.intelligent.alarm.server.common.util.LogUtils;
import lombok.extern.log4j.Log4j2;
import org.joda.time.DateTime;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.*;

/**
 *
 * @ClassName: DistributeQuartzManager
 * @Description Addition, deletion and modification of quartz timing task management in Distributed Cluster
 * @date 2019/10/1211:04
 */
@Log4j2
@Component
public class DistributeQuartzManager {

    @Autowired
    @Qualifier("schedulerFactoryBean")
    private SchedulerFactoryBean schedulerFactory;

    /**
     * Determine whether a job exists
     *
     * @param jobName
     *            Task name
     * @param jobGroupName
     *            Task group name
     * @return
     */
    public  boolean isExistJob(String jobName, String jobGroupName) {
        boolean exist = false;
        try {

            Scheduler sched = schedulerFactory.getScheduler();
            JobKey jobKey = new JobKey(jobName, jobGroupName);
            exist = sched.checkExists(jobKey);
        }
        catch (SchedulerException e) {
            e.printStackTrace();
        }
        if (exist) {
            log.debug("trigger[" + jobName + "]repeat");
        }
        else {
            log.debug("trigger[" + jobName + "]available");
        }
        return exist;

    }

    /**
     * @Description: Add a scheduled task
     *
     * @param jobName
     *            Task name
     * @param jobGroupName
     *            Task group name
     * @param triggerName
     *            Trigger Name 
     * @param triggerGroupName
     *            Trigger group name
     * @param jobClass
     *            task
     * @param cron
     *            For time setting, refer to the quartz documentation
     */
    public JobDetail addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName,
                                   @SuppressWarnings("rawtypes") Class jobClass, JobDataMap jMap, String cron) {
        return doAddJob(jobName, jobGroupName, triggerName, triggerGroupName, jobClass, jMap, cron);
    }

    private JobDetail doAddJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class jobClass, JobDataMap jMap, String cron) {
        JobDetail jobDetail = null;
        if(StringUtils.isEmpty(jobGroupName)){
            jobGroupName = Scheduler.DEFAULT_GROUP;
        }

        if(StringUtils.isEmpty(triggerGroupName)){
            triggerGroupName = Scheduler.DEFAULT_GROUP;
        }

        try {
            Scheduler sched = schedulerFactory.getScheduler();
            // Task name, task group, task execution class
            JobBuilder jobBuilder = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName);
            if(jMap != null && jMap.size() > 0){
                jobBuilder = jobBuilder.usingJobData(jMap);
            }
            jobDetail = jobBuilder.build();

            // trigger
            TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
            // Trigger name, trigger group
            triggerBuilder.withIdentity(triggerName, triggerGroupName);
            triggerBuilder.startNow();
            // Trigger time setting
            triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
            // Create Trigger object
            CronTrigger trigger = (CronTrigger) triggerBuilder.build();

            // The scheduling container sets JobDetail and Trigger
            sched.scheduleJob(jobDetail, trigger);
            Trigger.TriggerState triggerState = sched.getTriggerState(trigger.getKey());
            // |-NONE none NONE
            // |-NORMAL status
            // |-PAUSED status
            // |-COMPLETE complete
            // |-ERROR error
            // |-BLOCKED

            log.debug("JobName: " + jobName + ",state:" + triggerState + ",GroupName:" + jobGroupName);
            // start-up
            if (!sched.isShutdown()) {
                sched.start();
            }

            // Press the new trigger to reset the job execution
//            sched.rescheduleJob(trigger.getKey(), trigger);
        } catch (Exception e) {
            log.error("Exception occurred when adding a scheduled task:" +  e);
        }

        return jobDetail;
    }

    /**
     * Start a scheduled job. If the job has been started, stop and delete it first, and then add the start job again
     * @param jobName
     * @param jobGroupName
     * @param triggerName
     * @param triggerGroupName
     * @param jobClass
     * @param jMap
     * @param cron
     */
    public JobDetail startJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName,
                                     @SuppressWarnings("rawtypes") Class jobClass, JobDataMap jMap, String cron) {
        //There is a scheduled job. Delete it first
        if(isExistJob(jobName, jobGroupName) == true) {
            removeJob(jobName, jobGroupName, triggerName, triggerGroupName);
        }
        //Add and start a job
        return addJob(jobName, jobGroupName, triggerName, triggerGroupName, jobClass, jMap, cron);
    }

    public void startJob(JobDetail jobDetail, CronTrigger trigger) {
        try {
            Scheduler sched = schedulerFactory.getScheduler();
            // The scheduling container sets JobDetail and Trigger
            sched.scheduleJob(jobDetail, trigger);
            Trigger.TriggerState triggerState = sched.getTriggerState(trigger.getKey());
            // |-NONE none NONE
            // |-NORMAL status
            // |-PAUSED status
            // |-COMPLETE complete
            // |-ERROR error
            // |-BLOCKED


            log.info("addJob JobKey: " + jobDetail.getKey() + ",state:" + triggerState);
            // start-up
            if (!sched.isShutdown()) {
                sched.start();
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * @Description: Modify the trigger time of a task
     *
     * @param jobName
     * @param jobGroupName
     * @param triggerName
     *            Trigger Name 
     * @param triggerGroupName
     *            Trigger group name
     * @param cron
     *            For time setting, refer to the quartz documentation
     */
    public void modifyJobTime(String jobName, String jobGroupName, String triggerName, String triggerGroupName,
                                     @SuppressWarnings("rawtypes") Class jobClass, JobDataMap jMap, String cron) {
        /** Method 1: call rescheduleJob to start */
        // trigger
        // TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
        // Trigger name, trigger group
        // triggerBuilder.withIdentity(triggerName, triggerGroupName);
        // triggerBuilder.startNow();
        // Trigger time setting
        // triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
        // Create Trigger object
        // trigger = (CronTrigger) triggerBuilder.build();
        // Method 1: modify the trigger time of a task
        // sched.rescheduleJob(triggerKey, trigger);
        /** Method 1: call rescheduleJob to end */

        /** Method 2: delete first, and then create a new Job */
        removeJob(jobName, jobGroupName, triggerName, triggerGroupName);
        addJob(jobName, jobGroupName, triggerName, triggerGroupName, jobClass, jMap, cron);
        log.info(String.format("Modification[%s]Scheduled task succeeded!",jobName));

        /** Method 2: delete first, and then create a new Job */
    }

    /**
     * @Description: Remove a task
     *
     * @param jobName
     * @param jobGroupName
     * @param triggerName
     * @param triggerGroupName
     */
    public void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) {
        try {
           /* ApplicationContext context = SpringContextUtils.getApplicationContext();
            RedisDistributedLock redLock = context.getBean(RedisDistributedLock.class);
            String lockKey = DOS + CacheConstant.LOCK_KEY + CacheConstant.SEPARATOR + jobGroupName + CacheConstant.SEPARATOR + jobName + CacheConstant.SEPARATOR + "Execute";
            redLock.unlockAsync(lockKey);*/

            Scheduler sched = schedulerFactory.getScheduler();

            TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);

            sched.pauseTrigger(triggerKey);// Stop trigger
            sched.unscheduleJob(triggerKey);// Remove trigger
            sched.deleteJob(JobKey.jobKey(jobName, jobGroupName));// Delete task

            List<String> jobGroupNames = sched.getJobGroupNames();
            log.debug("Remove task group start-->groupsNames=[");
            for (String string : jobGroupNames) {
                GroupMatcher<JobKey> matcher = GroupMatcher.jobGroupEquals(string);
                Set<JobKey> jobKeys = sched.getJobKeys(matcher);
                log.debug(string + "Lower JOB by[");
                for (JobKey jobKey : jobKeys) {
                    log.debug(jobKey.getName() + ",");
                }
                log.debug("]");

            }
            log.debug("]End of removal task group.");
        } catch (Exception e) {
            log.error("remove job Task exception:" + e);
        }
    }

    public void getSchedulerStatus() {
        try {
                Scheduler scheduler = schedulerFactory.getScheduler();
                List<String> jobGroupNames = scheduler.getJobGroupNames();
                for (String jobGroupName : jobGroupNames) {
                    GroupMatcher<JobKey> matcher = GroupMatcher.jobGroupEquals(jobGroupName);
                    Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
                    for (JobKey jobKey : jobKeys) {
                        List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                        String cron = "";
                        for (Trigger trigger : triggers) {
                            if (trigger instanceof CronTrigger) {
                                CronTrigger cronTrigger = (CronTrigger) trigger;
                                cron = cronTrigger.getCronExpression();
                            }
                        }

                        log.info("-------------job name=" + jobKey.getName() + ",group name=" + jobGroupName + ",scheduler name=" + scheduler.getSchedulerName() + ",cron=" + cron);
                    }
                }
                List<JobExecutionContext> jobExecutionContexts = scheduler.getCurrentlyExecutingJobs();
                for(JobExecutionContext jobExecutionContext : jobExecutionContexts){
                    JobDetail jobDetail = jobExecutionContext.getJobDetail();
                    JobKey jobKey = jobDetail.getKey();
                    String fireTime = new DateTime(jobExecutionContext.getFireTime()).toString(JobConstant.DATE_TIME_FORMAT);
                    String previousTime = new DateTime(jobExecutionContext.getPreviousFireTime()).toString(JobConstant.DATE_TIME_FORMAT);
                    String nextFireTime = new DateTime(jobExecutionContext.getNextFireTime()).toString(JobConstant.DATE_TIME_FORMAT);
                    log.info("---------current running job key=" + jobKey.getName() + ",group name=" + jobKey.getGroup() + ",scheduler name=" + scheduler.getSchedulerName()
                            + LogUtils.formatScheduledJobLogInfo(jobExecutionContext) + ",class=" + jobKey.getClass().getSimpleName() +
                            ",description=" + jobDetail.getDescription());

                }

                Set<String> pauseGroupNames = scheduler.getPausedTriggerGroups();
                for (String jobGroupName : pauseGroupNames) {
                    GroupMatcher<JobKey> matcher = GroupMatcher.jobGroupEquals(jobGroupName);
                    Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
                    for (JobKey jobKey : jobKeys) {
                        log.info("-------------pause job name=" + jobKey.getName() + ",group name=" + jobGroupName + ",scheduler name=" + scheduler.getSchedulerName());
                    }
                }


        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * @Description:Start all scheduled tasks
     */
    public void startAllJobs() {
        try {
            Scheduler sched = schedulerFactory.getScheduler();
            sched.start();
        }
        catch (Exception e) {
            log.error("Exception occurred when starting all scheduled tasks:", e);
            throw new RuntimeException(e);
        }
    }

    public static Map<String,String> parseJobDataMap(String jsonStr){
        Map<String,String> map = new HashMap<>();
        if(StringUtils.isEmpty(jsonStr)){
            return map;
        }
        try{
            JSONObject json = JSONObject.parseObject(jsonStr);
            for (String key : json.keySet()) {
                String value = json.getString(key);
                map.put(key,value);
            }
        }catch (Exception e){
            log.error("parseJobDataMap error is: {}", e);
        }
        return map;
    }

    /**
     * @Description:Close all scheduled tasks
     */
    public void shutdownAllJobs() {
        try {
            Scheduler scheduler = schedulerFactory.getScheduler();
            if (!scheduler.isShutdown()) {
                scheduler.shutdown();
            }
        }
        catch (Exception e) {
            log.error("Exception occurred when closing all scheduled tasks:", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Schedule tasks to be scheduled
     * @return
     */
    public List<SysScheduleTaskDTO> queryAllJobs(){

        List<SysScheduleTaskDTO> jobConfigs = new ArrayList<>();
        try {
            Scheduler scheduler = schedulerFactory.getScheduler();
            for(String groupJob: scheduler.getJobGroupNames()){
                for(JobKey jobKey: scheduler.getJobKeys(GroupMatcher.groupEquals(groupJob))){
                    List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                    for (Trigger trigger: triggers) {
                        Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                        JobDetail jobDetail = scheduler.getJobDetail(jobKey);

                        SysScheduleTaskDTO jobConfig = new SysScheduleTaskDTO();
                        String cronExpression = "";
                        if (trigger instanceof CronTrigger) {
                            CronTrigger cronTrigger = (CronTrigger) trigger;
                            cronExpression = cronTrigger.getCronExpression();
                            TriggerKey triggerKey =cronTrigger.getKey();
                            jobConfig.setTriggerName(triggerKey.getName());
                            jobConfig.setTriggerGroupName(triggerKey.getGroup());
                        }

                        Class jobClazz = jobDetail.getJobClass();
                        String classCode = JobClassEnum.getCodeByClass(jobClazz);
                        jobConfig.setJobClass(classCode);
                        jobConfig.setJobName(jobKey.getName());
                        jobConfig.setJobGroupName(jobKey.getGroup());
                        jobConfig.setDescription(jobDetail.getDescription());
                        jobConfig.setStatus(triggerState.name());
                        jobConfig.setCron(cronExpression);
                        jobConfigs.add(jobConfig);
                    }
                }
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
            log.error("Exception occurred when querying all scheduled tasks:", e);
            throw new RuntimeException(e);
        }
        return jobConfigs;
    }

    /**
     * Running tasks
     * @return
     */
    public  List<SysScheduleTaskDTO> getRunningJobs(){
        List<SysScheduleTaskDTO> jobList = new ArrayList<>();
        try {
            Scheduler scheduler = schedulerFactory.getScheduler();
            List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();

            for (JobExecutionContext executingJob : executingJobs) {
                SysScheduleTaskDTO job = new SysScheduleTaskDTO();
                Trigger trigger = executingJob.getTrigger();
                Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                TriggerKey triggerKey =trigger.getKey();
                job.setTriggerName(triggerKey.getName());
                job.setTriggerGroupName(triggerKey.getGroup());

                JobDetail jobDetail = executingJob.getJobDetail();
                JobKey jobKey = jobDetail.getKey();

                Class jobClazz = jobDetail.getJobClass();
                String classCode = JobClassEnum.getCodeByClass(jobClazz);
                job.setJobClass(classCode);
                job.setJobName(jobKey.getName());
                job.setJobGroupName(jobKey.getGroup());
                job.setDescription(jobDetail.getDescription());
                job.setStatus(triggerState.name());
                if (trigger instanceof CronTrigger) {
                    CronTrigger cronTrigger = (CronTrigger) trigger;
                    String cronExpression = cronTrigger.getCronExpression();
                    job.setCron(cronExpression);
                }
                job.setDescription("trigger:" + trigger.getKey());
                jobList.add(job);
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return jobList;
    }
}

1.7 startup procedure

quartz cluster is different from other distributed clusters. Cluster instances do not need to communicate with each other, but only need to interact with the DB. Other forces are sensed through the DB to realize Job scheduling. Therefore, it only needs to be started according to ordinary java programs. For capacity expansion, it only needs to start new instances without additional configuration.

Posted by ravi.kinjarapu on Thu, 28 Oct 2021 02:44:00 -0700