1. Basic implementation principle of Quartz task scheduling
Quartz is an open source project in the field of task scheduling by OpenSymphony, which is completely based on Java implementation. As an excellent open source scheduling framework, Quartz has the following features:
(1) Powerful scheduling functions, such as supporting a variety of scheduling methods, can meet various conventional and special needs;
(2) Flexible application methods, such as supporting multiple combinations of tasks and scheduling, and supporting multiple storage methods of scheduling data;
(3) Distributed and clustering capabilities, Terracotta has further improved its original functions after the acquisition. This article will add up to this part.
1.1 Quartz core elements
The core elements of Quartz task scheduling are: Scheduler-task scheduler, Trigger-trigger, and Job-task. where trigger and job are metadata for task scheduling, and scheduler is the controller that actually performs scheduling.
Trigger is an element used to define the scheduling time, that is, according to which time rules the task is executed. There are four types of triggers in Quartz: SimpleTrigger, CronTirgger, DateIntervalTrigger, and NthIncludedDayTrigger. These four triggers can meet most of the needs of enterprise applications.
Job is used to represent scheduled tasks. There are mainly two types of jobs: stateless and stateful. For the same trigger, stateful jobs cannot be executed in parallel. Only after the task triggered last is executed can the next execution be triggered. Job has two main attributes: volatile and durability, where volatile means whether the task is persisted to the database storage, while durability means whether the task is retained when there is no trigger association. Both are persisted or preserved when the value is true. A job can be associated with multiple triggers, but a trigger can only associate one job.
Scheduler is created by scheduler factory: DirectSchedulerFactory or StdSchedulerFactory. The second factory, StdSchedulerFactory, is used more frequently because DirectSchedulerFactory is not convenient enough to use, and many detailed manual coding settings are required. There are three main types of Scheduler: RemoteMBeanScheduler, RemoteScheduler and StdScheduler.
The relationship between the core elements of Quartz is shown in Figure 1.1:
Figure 1.1 Core element relationship diagram
1.2 Quartz thread view
In Quartz, there are two types of threads, Scheduler scheduler threads and task execution threads, where task execution threads usually use a thread pool to maintain a group of threads.
Figure 1.2 Quartz thread view
There are two main threads for Scheduler: threads that perform regular scheduling and threads that execute misfiredtrigger. The regular dispatch thread polls all triggers stored. If there is a trigger that needs to be triggered, that is, the time of the next trigger has reached, it obtains an idle thread from the task execution thread pool to execute the task associated with the trigger. The Misfire thread scans all triggers to see if there is a misfiredtrigger. If so, it is handled separately according to the misfire policy (fire now OR wait for the next fire).
1.3 Quartz Job Data Storage
The triggers and jobs in Quartz need to be stored before they can be used. There are two storage methods in Quartz: RAMJobStore and JobStoreSupport, where RAMJobStore stores triggers and jobs in memory, while JobStoreSupport stores triggers and jobs in database based on jdbc. RAMJobStore is very fast access, but since all data will be lost after the system is stopped, JobStoreSupport must be used in cluster applications.
2. Quartz cluster principle 2.1 Quartz cluster architecture
Each node in a Quartz cluster is an independent Quartz application, which in turn manages other nodes. This means you have to start or stop each node separately. In the Quartz cluster, independent Quartz nodes do not communicate with another node or management node, but instead perceive another Quartz application through the same database table, as shown in Figure 2.1.
Figure 2.1 Quartz cluster architecture
2.2 Quartz cluster-related database tables
Because the Quartz cluster depends on the database, it is necessary to first create the Quartz database table. The Quartz release package includes SQL scripts for all supported database platforms. These SQL scripts are stored in the <quartz_home>/docs/dbTables directory. The Quartz version 1.8.4 used here has a total of 12 tables. The number of tables may be different in different versions. The database is mysql, and use tables_mysql.sql to create a database table. All tables are shown in Figure 2.2, and a brief introduction to these tables is shown in Figure 2.3.
Figure 2.2 Tables generated in Quartz 1.8.4 in mysql database
Figure 2.3 Introduction to Quartz data table
2.2.1 Scheduler status table (QRTZ_SCHEDULER_STATE)
Description: The node instance information in the cluster, Quartz reads the information of this table regularly to determine the current status of each instance in the cluster.
instance_name: The name configured by org.quartz.scheduler.instanceId in the configuration file. If set to AUTO, quartz will generate a name based on the physical machine name and the current time.
last_checkin_time: last check-in time
checkin_interval: Check-in interval time
2.2.2 Trigger and task association table (qrtz_fired_triggers)
Stores status information related to the triggered Trigger and execution information of the associated job.
2.2.3 Trigger information table (qrtz_triggers)
trigger_name: trigger name, user can customize the name at will, no forced requirements
trigger_group: The name of the trigger group, which the user can customize at will, and there is no forced requirement.
job_name: foreign key of qrtz_job_details table job_name
job_group: qrtz_job_details table job_group foreign key
trigger_state: The current trigger status is set to ACQUIRED. If it is set to WAITING, the job will not trigger.
trigger_cron: trigger type, using cron expression
2.2.4 Task details table (qrtz_job_details)
Note: Save the job details, the table needs to be initialized by the user according to the actual situation
job_name: The name of the job in the cluster. The user can customize the name at will, without any forced requirements.
job_group: The name of the group to which the job belongs in the cluster, which is customized by the user at will, and there are no forced requirements.
job_class_name: The complete package name of the job implementation class in the cluster. Quartz finds the job class based on this path to classpath.
is_durable: Whether to persist, set this property to 1, quartz will persist the job to the database
job_data: A blob field that stores persisted job objects.
2.2.5 Permission Information Table (qrtz_locks)
Note: There is a corresponding dml initialization in tables_oracle.sql, as shown in Figure 2.4.
Figure 2.4 Initialization information in Quartz permission information table
2.3 Quartz Scheduler startup process in the cluster
Quartz Scheduler itself does not notice that it is clustered, and only the JDBC JobStore configured for Scheduler will know. When the Quartz Scheduler starts, it calls the schedulerStarted() method of the JobStore, which tells the JobStore Scheduler that it has started. The schedulerStarted() method is implemented in the JobStoreSupport class. The JobStoreSupport class determines whether the Scheduler instance participates in the cluster based on the settings in the quartz.properties file. If the cluster is configured, an instance of a new ClusterManager class will be created, initialized and started. ClusterManager is an inline class in the JobStoreSupport class, inheriting java.lang.Thread, it runs regularly and performs check-in functions on the Scheduler instance. Scheduler also needs to check whether any other cluster nodes have failed. The check-in operation execution cycle is configured in quartz.properties.
2.4 Detecting a failed Scheduler node
When a Scheduler instance performs a check-in, it checks whether other Scheduler instances have not been checked in at the time they expected. This is determined by checking whether the value of the Scheduler recorded in the LAST_CHEDK_TIME column in the SCHEDULER_STATE table is earlier than org.quartz.jobStore.clusterCheckinInterval. If one or more nodes have not been checked in at a predetermined time, then the running Scheduler assumes that they have failed.
2.5 Recovering Jobs from Failed Instances
When a Sheduler instance fails when executing a job, it is possible that another working Scheduler instance will take the job and run it again. To achieve this behavior, the Job recovery property configured to the JobDetail object must be set to true (job.setRequestsRecovery(true)). If the recoverable property is set to false (default is false), it will not rerun when a Scheduler fails to run the job; it will be triggered by another Scheduler instance at the next trigger time. How quickly the Scheduler instance can be detected after a failure depends on the check-in interval of each Scheduler (i.e. org.quartz.jobStore.clusterCheckinInterval mentioned in 2.3).
3. Quartz cluster instance (Quartz+Spring)
3.1 Spring incompatible with Quartz issues
Spring no longer supports Quartz since 2.0.2. Specifically, when Quartz+Spring instantiates Quartz's Task into the database, a Serializable error will occur:
<bean id="jobtask"> <property name="targetObject"> <ref bean="quartzJob"/> </property> <property name="targetMethod"> <value>execute</value> </property></bean>
The methodInvoking method in the MethodInvokingJobDetailFactoryBean class does not support serialization, so it will throw an error when serializing the TASK of QUARTZ into the database.
First, solve the problem of MethodInvokingJobDetailFactoryBean. Without modifying the Spring source code, you can avoid using this class and directly call JobDetail. However, using JobDetail implementation requires you to implement the logic of MothodInvoking by yourself. You can use the jobClass and JobDataAsMap properties of JobDetail to customize a Factory (Manager) to achieve the same purpose. For example, in this example, a new MyDetailQuartzJobBean is created to implement this function.
3.2 MyDetailQuartzJobBean.java file
package org.lxh.mvc.jobbean;import java.lang.reflect.Method;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.springframework.context.ApplicationContext;import org.springframework.scheduling.quartz.QuartzJobBean;public class MyDetailQuartzJobBean extends QuartzJobBean { protected final Log logger = LogFactory.getLog(getClass()); private String targetObject; private String targetMethod; private ApplicationContext ctx; protected void executeInternal(JobExecutionContext context) throws JobExecutionException { try { logger.info("execute [" + targetObject + "] at once>>>>>"); Object oargetObject = ctx.getBean(targetObject); Method m = null; try { m = oargetObject.getClass().getMethod(targetMethod, new Class[] {}); m.invoke(oargetObject, new Object[] {}); } catch (SecurityException e) { logger.error(e); } catch (NoSuchMethodException e) { logger.error(e); } } catch (Exception e) { throw new JobExecutionException(e); } } public void setApplicationContext(ApplicationContext applicationContext){ this.ctx=applicationContext; } public void setTargetObject(String targetObject) { this.targetObject = targetObject; } public void setTargetMethod(String targetMethod) { this.targetMethod = targetMethod; }}3.3 Real Job implementation class
In the Test class, the function of printing the current time of the system is simply implemented.
package org.lxh.mvc.job;import java.io.Serializable;import java.util.Date;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;public class Test implements Serializable{ private Log logger = LogFactory.getLog(Test.class); private static final long serialVersionUID = -2073310586499744415L; public void execute () { Date date=new Date(); System.out.println(date.toLocaleString()); }}3.4 Configure the quartz.xml file
<bean id="Test" scope="prototype"> </bean> <bean id="TestjobTask"> <property name="jobClass"><value>org.lxh.mvc.jobbean.MyDetailQuartzJobBean</value> </property> <property name="jobDataAsMap"> <map> <entry key="targetObject" value="Test" /> <entry key="targetMethod" value="execute" /> </map> </property> </bean> <bean name="TestTrigger"> <property name="jobDetail" ref="TestjobTask" /> <property name="cronExpression" value="0/1 * * * * ?" /> </bean> <bean id="quartzScheduler"> <property name="configLocation" value="classpath:quartz.properties"/> <property name="triggers"> <list> <ref bean="TestTrigger" /> </list> </property> <property name="applicationContextSchedulerContextKey" value="applicationContext" /> </bean>
3.5 Test
The code and configuration of ServerA and ServerB are exactly the same. Start ServerA first and then start ServerB. After Server is shut down, ServerB will monitor its shutdown and take over the job that is executing on ServerA and continue execution.
4. Quartz cluster instance (Single Quartz)
Although we have implemented the cluster configuration of Spring+Quartz, it is still not recommended to use this method due to compatibility issues between Spring and Quartz. In this section, we implemented a cluster configured separately with Quartz, which is simple and stable compared to Spring+Quartz.
4.1 Engineering Structure
We use Quartz alone to implement its cluster functions, code structure and required third-party jar packages as shown in Figure 3.1. Among them, Mysql version: 5.1.52, and Mysql driver version: mysql-connector-java-5.1.5-bin.jar (For 5.1.52, it is recommended to use this version of the driver because there is a bug in Quartz that it cannot run normally when combined with some Mysql drivers).
Figure 4.1 Quartz cluster engineering structure and required third-party jar packages
Among them, quartz.properties is the Quartz configuration file and placed in the src directory. If there is no such file, Quartz will automatically load the quartz.properties file in the jar package; SimpleRecoveryJob.java and SimpleRecoveryStatefulJob.java are two jobs; ClusterExample.java writes scheduling information, triggering mechanism and corresponding test main functions.
4.2 Configuration file quartz.properties
The default file name quartz.properties is used to activate the cluster feature by setting the "org.quartz.jobStore.isClustered" property to "true". Each instance in the cluster must have a unique "instance id" ("org.quartz.scheduler.instanceId" property), but it should have the same "scheduler instance name" ("org.quartz.scheduler.instanceName"), which means that each instance in the cluster must use the same quartz.properties configuration file. Except for the following exceptions, the contents of the configuration file must be the same:
a. Thread pool size.
b. Different "org.quartz.scheduler.instanceId" attribute values (just set to "AUTO").
#============================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================== AUTO#================================================================================================================================================================================================== org.quartz.jobStore.impl.jdbcjobstore.JobStoreTXorg.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegateorg.quartz.jobStore.tablePrefix = QRTZ_org.quartz.jobStore.isClustered = trueorg.quartz.jobStore.clusterCheckinInterval = 10000 org.quartz.jobStore.dataSource = myDS #======================================================================================================================================================================================================================================================================================================================================================================================================= #============================================================================ org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driverorg.quartz.dataSource.myDS.URL = jdbc:mysql://192.168.31.18:3306/test?useUnicode=true&characterEncoding=UTF-8org.quartz.dataSource.myDS.user = rootorg.quartz.dataSource.myDS.password = 123456org.quartz.dataSource.myDS.maxConnections = 30#============================================================================================================================================================================================================================================================================================================================================================================================================================================================================================= org.quartz.simpl.SimpleThreadPoolorg.quartz.threadPool.threadCount = 5org.quartz.threadPool.threadPriority = 5org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
4.3 ClusterExample.java file
package cluster;import java.util.Date;import org.quartz.JobDetail;import org.quartz.Scheduler;import org.quartz.SchedulerFactory;import org.quartz.SimpleTrigger;import org.quartz.impl.StdSchedulerFactory;public class ClusterExample { public void cleanUp(Scheduler inScheduler) throws Exception { System.out.println("**** Deleting existing jobs/triggers *****"); // unschedule jobs String[] groups = inScheduler.getTriggerGroupNames(); for (int i = 0; i < groups.length; i++) { String[] names = inScheduler.getTriggerNames(groups[i]); for (int j = 0; j < names.length; j++) { inScheduler.unscheduleJob(names[j], groups[i]); } } // delete jobs groups = inScheduler.getJobGroupNames(); for (int i = 0; i < groups.length; i++) { String[] names = inScheduler.getJobNames(groups[i]); for (int j = 0; j < names.length; j++) { inScheduler.deleteJob(names[j], groups[i]); } } } public void run(boolean inClearJobs, boolean inScheduleJobs) throws Exception { // First we must get a reference to a scheduler SchedulerFactory sf = new StdSchedulerFactory(); Scheduler sched = sf.getScheduler(); if (inClearJobs) { cleanUp(sched); } System.out.println("-------------------------------"); if (inScheduleJobs) { System.out.println("------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- re-execute this job if it was in progress when // the scheduler went down... job.setRequestsRecovery(true); SimpleTrigger trigger = new SimpleTrigger("triger_" + count, schedId, 200, 1000L); trigger.setStartTime(new Date(System.currentTimeMillis() + 1000L)); System.out.println(job.getFullName() + " will run at: " + trigger.getNextFireTime() + " and repeat: " + trigger.getRepeatCount() + " times, every " + trigger.getRepeatInterval() / 1000 + " seconds"); sched.scheduleJob(job, trigger); count++; job = new JobDetail("job_" + count, schedId, SimpleRecoveryStatefulJob.class); // ask scheduler to re-execute this job if it was in progress when // the scheduler went down... job.setRequestsRecovery(false); trigger = new SimpleTrigger("trig_" + count, schedId, 100, 2000L); trigger.setStartTime(new Date(System.currentTimeMillis() + 2000L)); System.out.println(job.getFullName() + " will run at: " + trigger.getNextFireTime() + " and repeat: " + trigger.getRepeatCount() + " times, every " + trigger.getRepeatInterval() / 1000 + " seconds"); sched.scheduleJob(job, trigger); } // jobs don't start firing until start() has been called... System.out.println("----------------------------------------"); sched.start(); System.out.println("-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Thread.sleep(3600L * 1000L); } catch (Exception e) { } System.out.println("-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- args.length; i++) { if (args[i].equalsIgnoreCase("clearJobs")) { clearJobs = true; } else if (args[i].equalsIgnoreCase("dontScheduleJobs")) { scheduleJobs = false; } } ClusterExample example = new ClusterExample(); example.run(clearJobs, scheduleJobs); }}4.4 SimpleRecoveryJob.java
package cluster;import java.io.Serializable;import java.util.Date;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.quartz.Job;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;//If there are actions and jobs that you want to execute repeatedly, write the relevant code in the execute method, the premise is: implement the Job interface//As for when the SimpleJob class is instantiated and when the execute method is called, we don’t need to pay attention to it, and leave it to Quartzpublic class SimpleRecoveryJob implements Job, Serializable { private static Log _log = LogFactory.getLog(SimpleRecoveryJob.class); public SimpleRecoveryJob() { } public void execute(JobExecutionContext context) throws JobExecutionException { //This job is simply printing out the job name and the time when the job is running String jobName = context.getJobDetail().getFullName(); System.out.println("JOB 11111111111111111 SimpleRecoveryJob says: " + jobName + " executing at " + new Date()); }}4.5 Operation results
The configuration and code in Server A and Server B are exactly the same. Running method: Run ClusterExample.java on any host, add the task to the schedule, and observe the run results:
Run ServerA, the results are shown in Figure 4.2.
Figure 4.2 ServerA operation result 1
After turning on ServerB, the output of ServerA and ServerB is shown in Figures 4.3 and 4.4.
Figure 4.3 ServerA running result 2
Figure 4.4 ServerB operation result 1
It can be seen from Figures 4.3 and 4.4 that after ServerB is turned on, the system automatically realizes responsibility for balance, and ServerB takes over Job1. After shutting down ServerA, the running results of ServerB are shown in Figure 4.5.
Figure 4.5 ServerB operation result 2
As can be seen from Figure 4.5, ServerB can detect that ServerA is lost, take over the task Job2, and lose ServerA to the Job2 that needs to be executed during this exception time.
5. Things to note
5.1 Time synchronization problem
Quartz doesn't actually care whether you run nodes on the same or different machines. When a cluster is placed on different machines, it is called a horizontal cluster. When a node runs on the same machine, it is called a vertical cluster. For vertical clusters, there is a problem of single point of failure. This is unacceptable for high availability applications, because once the machine crashes, all nodes are terminated. For horizontal clusters, there is a problem of time synchronization.
The node uses a timestamp to notify other instances of its own last check-in time. If the node's clock is set to future time, the running Scheduler will no longer realize that the node has fallen. On the other hand, if the clock of a node is set to past time, perhaps the other node will determine that the node has fallen out and try to take its job and rerun. The easiest way to synchronize a computer clock is to use an Internet Time Server ITS.
5.2 The problem of nodes competing for jobs
Because Quartz uses a random load balancing algorithm, the job is executed by different instances in a random way. Quartz's official website mentioned that at present, there is no method to assign (pin) a job to a specific node in the cluster.
5.3 Issue to get job list from cluster
Currently, if you do not enter the database query directly, there is no simple way to get a list of all the executing jobs in the cluster. Requesting a Scheduler instance will only get a list of jobs running on that instance. Quartz official website recommends that you can get all the job information from the corresponding table by writing some JDBC code to access the database.
Summarize
The above is the entire content of this article. I hope that the content of this article has certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support to Wulin.com.