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.