1 Introduction
Recently, our company has launched a distributed task scheduling framework: XXL-JOB to facilitate the management and control of tasks. Originally, I wanted to talk about the framework, but in the process of learning and understanding, I found that the framework was Quartz
developed and implemented based on ideas. It is Quartz
a very popular open source task scheduling framework. Java
Or the reference standard, so let's talk about the framework first Quartz
.
1.1. What is Quartz
Quartz is another open source project of the OpenSymphony open source organization in the field of Job scheduling. It is an open source task schedule management system completely developed by java. A system that executes (or notifies) other software components. Its functionality is similar to java.util.Timer. But compared to Timer, Quartz has added many functions. As an excellent open source scheduling framework, Quartz has the following characteristics:
- Powerful scheduling functions, such as supporting a variety of scheduling methods, can meet various conventional and special needs
- Flexible application methods, supporting multiple storage methods for scheduling data
- Distributed and Cluster Capabilities
1.2. Storage method
The comparison between RAMJobStore and JDBCJobStore is as follows:
type | advantage | shortcoming |
---|---|---|
RAMJobStore | No external database, easy to configure, fast to run | Because the scheduler information is stored in memory allocated to the JVM, all scheduling information is lost when the application stops running. In addition, because it is stored in the JVM memory, the number of Jobs and Triggers that can be stored will be limited. |
JDBCJobStore | Support clusters , because all task information will be saved in the database, you can control things, and if the application server is shut down or restarted, the task information will not be lost, and you can restore tasks that fail to execute due to server shutdown or restart | The speed of operation depends on the speed of connecting to the database |
According to the above, if you want to support distributed clusters, you must belong to it JDBCJobStore
. It needs to rely on the database MySQL, the database initialization table SQL download: tables , and the table description is as follows:
Table Name | illustrate |
---|---|
qrtz_blob_triggers | Trigger is stored as a Blob type (used when Quartz users use JDBC to create their own custom Trigger type, and the JobStore does not know how to store the instance) |
qrtz_calendars | Quartz's Calendar calendar information is stored in Blob type. Quartz can configure a calendar to specify a time range |
qrtz_cron_triggers | Stores Cron Triggers, including Cron expressions and time zone information |
qrtz_fired_triggers | Store the status information related to the triggered Trigger, as well as the execution information of the associated Job |
qrtz_job_details | Store the details of each configured Job |
qrtz_locks | Stores information about non-pessimistic locks of the program (if pessimistic locks are used) |
qrtz_paused_trigger_graps | Stores information about paused Trigger groups |
qrtz_scheduler_state | Store a small amount of state information about the Scheduler, and other Scheduler instances (if used in a cluster) |
qrtz_simple_triggers | Store a simple Trigger, including the number of repetitions, intervals, and the number of touches |
qrtz_triggers | Store configured Trigger information |
Project recommendation : Based on the encapsulation of the underlying framework of SpringBoot2.x, SpringCloud and SpringCloudAlibaba enterprise-level system architecture, it solves common non-functional requirements during business development, prevents repeated wheel creation, and facilitates rapid business development and unified management of enterprise technology stack frameworks. Introduce the idea of componentization to achieve high cohesion and low coupling and highly configurable, so as to be pluggable. Strictly control package dependencies and unified version management to minimize dependencies. Pay attention to code specifications and comments, very suitable for personal learning and enterprise use
Github address : https://github.com/plasticene/plasticene-boot-starter-parent
Gitee address : https://gitee.com/plasticene3/plasticene-boot-starter-parent
WeChat public account : Shepherd Advanced Notes
Exchange discussion group: Shepherd_126
2. springboot integration example
It is very simple to integrate quartz with springboot. Here we demonstrate the cluster mode, so using it JDBCJobStore
, the related dependencies are as follows:
<!-- 实现对 Quartz 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
Before creating a task, we need to download the above SQL statement and execute it. Here, the table can be built in the same database as the business database, or it can be placed in a separate database. If the database and table are built separately, then the business service is a multi-data source. The data source connection needs to be repackaged. The following multi-data source configuration:
@Configuration
public class DataSourceConfiguration {
/**
* 创建 user 数据源的配置对象
*/
@Primary
@Bean(name = "userDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.user") // 读取 spring.datasource.user 配置到 DataSourceProperties 对象
public DataSourceProperties userDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 创建 user 数据源
*/
@Primary
@Bean(name = "userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user.hikari") // 读取 spring.datasource.user 配置到 HikariDataSource 对象
public DataSource userDataSource() {
// 获得 DataSourceProperties 对象
DataSourceProperties properties = this.userDataSourceProperties();
// 创建 HikariDataSource 对象
return createHikariDataSource(properties);
}
/**
* 创建 quartz 数据源的配置对象
*/
@Bean(name = "quartzDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.quartz") // 读取 spring.datasource.quartz 配置到 DataSourceProperties 对象
public DataSourceProperties quartzDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 创建 quartz 数据源
*/
@Bean(name = "quartzDataSource")
@ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
@QuartzDataSource
public DataSource quartzDataSource() {
// 获得 DataSourceProperties 对象
DataSourceProperties properties = this.quartzDataSourceProperties();
// 创建 HikariDataSource 对象
return createHikariDataSource(properties);
}
private static HikariDataSource createHikariDataSource(DataSourceProperties properties) {
// 创建 HikariDataSource 对象
HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
// 设置线程池名
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
In order to test quickly and easily, we put Quartz's built table together with the business library, and then configure it as follows:
spring:
datasource:
url: jdbc:mysql://10.10.0.10:3306/ptc_job?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
# Quartz 的配置,对应 QuartzProperties 配置类
quartz:
scheduler-name: clusteredScheduler # Scheduler 名字。默认为 schedulerName
job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。
auto-startup: true # Quartz 是否自动启动
startup-delay: 0 # 延迟 N 秒启动
wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
overwrite-existing-jobs: true # 是否覆盖已有 Job 的配置,注意为false时,修改已存在的任务调度cron,周期不生效
jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置
initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档
org:
quartz:
# JobStore 相关配置
jobStore:
# 数据源名称
dataSource: quartzDataSource # 使用的数据源
class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_ # Quartz 表前缀
isClustered: true # 是集群模式
clusterCheckinInterval: 1000
useProperties: false
# 线程池相关配置
threadPool:
threadCount: 25 # 线程池大小。默认为 10 。
threadPriority: 5 # 线程优先级
class: org.quartz.simpl.SimpleThreadPool # 线程池类型
create taskJob1
@DisallowConcurrentExecution
public class Job1 extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(getClass());
private static SimpleDateFormat fullDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final AtomicInteger count = new AtomicInteger();
@Autowired
private DemoService demoService;
private String k1;
public void setK1(String k1) {
this.k1 = k1;
}
@Override
protected void executeInternal(JobExecutionContext context) {
logger.info("[job1的执行了,时间: {}, k1={}, count={}, demoService={}]", fullDateFormat.format(new Date()), k1,
count.incrementAndGet(), demoService);
}
}
Inherit the QuartzJobBean abstract class, implement #executeInternal(JobExecutionContext context)
the method, and execute the logic of the custom scheduled task.
QuartzJobBean implements org.quartz.Job
the interface, which provides dependency property injection of the JobDataMap data into the Job Bean every time Quartz creates a Job to execute timing logic.
// QuartzJobBean.java
public final void execute(JobExecutionContext context) throws JobExecutionException {
try {
// 将当前对象,包装成 BeanWrapper 对象
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
// 设置属性到 bw 中
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValues(context.getScheduler().getContext());
pvs.addPropertyValues(context.getMergedJobDataMap());
bw.setPropertyValues(pvs, true);
} catch (SchedulerException ex) {
throw new JobExecutionException(ex);
}
// 执行提供给子类实现的抽象方法
this.executeInternal(context);
}
protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
The injection job task configuration is as follows:
@Bean
public JobDetail job1() {
return JobBuilder.newJob(Job1.class)
.withIdentity("job1")
.storeDurably()
.usingJobData("k1", "v1")
.build();
}
@Bean
public Trigger simpleJobTrigger() {
// 简单的调度计划的构造器
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(30) // 频率 30s执行一次。
.repeatForever(); // 次数。
// Trigger 构造器
return TriggerBuilder.newTrigger()
.forJob(job1())
.withIdentity("job1Trigger")
.withSchedule(scheduleBuilder)
.build();
}
At this time, start the project to view the log as follows:
2022-09-20 23:17:33.500 INFO 18982 --- [eduler_Worker-2] : [job1的执行了,时间: 2022-09-20 23:17:33, k1=v1, count=1, demoService=DemoService@3258ebff]
2022-09-20 23:18:03.463 INFO 18982 --- [eduler_Worker-3] : [job1的执行了,时间: 2022-09-20 23:18:03, k1=v1, count=1, demoService=DemoService@3258ebff]
2022-09-20 23:18:33.439 INFO 18982 --- [eduler_Worker-4] : [job1的执行了,时间: 2022-09-20 23:18:33, k1=v1, count=1, demoService=DemoService@3258ebff]
2022-09-20 23:19:03.448 INFO 18982 --- [eduler_Worker-5] : [job1的执行了,时间: 2022-09-20 23:19:03, k1=v1, count=1, demoService=DemoService@3258ebff]
count
It can be seen from the counter that each time Job0 will create a new Job object by Quartz to execute the task , but DemoService
the attribute values are the same, it is a Spring singleton bean, and the data of JobData is automatically mapped and injected into the task bean attribute.
The above is to execute tasks at a specified frequency through a simple scheduler simpleSchedule
. Of course, I can also use mainstream cron expressions to implement task cycle execution:
@Bean
public JobDetail job1() {
return JobBuilder.newJob(Job1.class)
.withIdentity("job1")
.storeDurably()
.usingJobData("k1", "v1")
.build();
}
@Bean
public Trigger cronJobTrigger() {
// 每隔1分钟执行一次
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0 0/1 * * * ? *");
// Trigger 构造器
return TriggerBuilder.newTrigger()
.forJob(job1())
.withIdentity("job1Trigger")
.withSchedule(scheduleBuilder)
.build();
}
The task scheduling execution result will not be displayed here, it is the same as above.
3. Implementation principle
Quartz operates tasks through the Scheduler scheduler. It can add task JobDetail and trigger Trigger to the task pool, delete tasks, or stop tasks. The scheduler puts these tasks and triggers into a JobStore. Here jobStore has memory form and persistent form. Of course, it can also be customized and expanded into an independent service.
Quartz internally uses a scheduling thread QuartzSchedulerThread to continuously find tasks that need to be executed next time in the JobStore, and encapsulates these tasks into a thread pool ThreadPool to run. The component structure is as follows:
core class
QuartzSchedulerThread
: The thread responsible for executing the work that triggers the Trigger registered with QuartzScheduler. ThreadPool:Scheduler
Using a thread pool as the infrastructure for task execution, tasks provide operational efficiency by sharing threads in the thread pool. QuartzSchedulerResources
: Contains all the resources (JobStore, ThreadPool, etc.) needed to create a QuartzScheduler instance. SchedulerFactory
: Provides a mechanism for obtaining a client-side usable handle to a scheduler instance. JobStore
: Interfaces implemented by classes that provide an org.quartz.Job and org.quartz.Trigger storage mechanism for the use of org.quartz.core.QuartzScheduler. The storage of jobs and triggers should be unique by the combination of their name and group. QuartzScheduler
: This is the core of Quartz, which is an indirect implementation of the org.quartz.Scheduler interface, including methods for scheduling org.quartz.Jobs, registering org.quartz.JobListener instances, etc. Scheduler
: This is the main interface of Quartz Scheduler, which represents an independent running container. The scheduler maintains a registry of JobDetails and Triggers. Once registered, the scheduler is responsible for executing jobs when their associated triggers fire (when their scheduled time arrives). Trigger
: A basic interface with common properties for all triggers, describing the time trigger rules for job execution. - Use TriggerBuilder to instantiate the actual trigger. JobDetail
: Passes the details attribute for the given job instance. JobDetails will be created/defined using JobBuilder. Job
: Interface to be implemented by classes representing "jobs" to be performed. There is only one method void execute(jobExecutionContext context) (jobExecutionContext provides various information of the scheduling context, and the runtime data is saved in the jobDataMap)
Job has a sub-interface StatefulJob, which represents a stateful task. Stateful tasks cannot be concurrent. The previous task has not been executed, and the subsequent task is blocked and waits. The following shows the native Quartz creating tasks, binding triggers, registering tasks and timers, and starting the scheduler.
/**
* 原生创建任务流程示例,有助于分析quartz实现原理
* @throws SchedulerException
*/
public static void test() throws SchedulerException {
//1.创建Scheduler的工厂
SchedulerFactory sf = new StdSchedulerFactory();
//2.从工厂中获取调度器实例
Scheduler scheduler = sf.getScheduler();
//3.创建JobDetail
JobDetail jb = JobBuilder.newJob(Job1.class)
.withDescription("this is a job") //job的描述
.withIdentity("job1", "test-job") //job 的name和group
.build();
//任务运行的时间,SimpleSchedule类型触发器有效
long time= System.currentTimeMillis() + 3*1000L; //3秒后启动任务
Date statTime = new Date(time);
//4.创建Trigger
//使用SimpleScheduleBuilder或者CronScheduleBuilder
Trigger t = TriggerBuilder.newTrigger()
.withDescription("")
.withIdentity("job1Trigger", "job1TriggerGroup")
//.withSchedule(SimpleScheduleBuilder.simpleSchedule())
.startAt(statTime) //默认当前时间启动
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?")) //10秒执行一次
.build();
//5.注册任务和定时器
scheduler.scheduleJob(jb, t);//源码分析
//6.启动 调度器
scheduler.start();
}
public static void main(String[] args) throws SchedulerException {
test();
}
Next, analyze the main three steps: creating a scheduler, registering tasks and triggers, and starting the scheduler to execute tasks
Scheduler initialization
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
SchedulerFacotory
It is a factory interface for creating a scheduler. It has two implementations. StdSchedulerFacotory
Create a Scheduler according to the configuration file, DirectSchedulerFactory
mainly through coding to control the Scheduler. Usually, in order to be less intrusive and more convenient to implement, we use StdSchedulerFacotory
the type to create the StdScheduler, in quartz.properties The configurations are all corresponding to this StdSchedulerFactory, so if you don't understand the default value of a certain configuration, you can look at StdSchedulerFactory
the code to obtain the configuration.
From sf.getScheduler()
the start, StdSchedulerFacotory
you can see the logic of the method by entering:
public Scheduler getScheduler() throws SchedulerException {
// 第一步:加载配置文件,System的properties覆盖前面的配置
if (cfg == null) {
initialize();
}
SchedulerRepository schedRep = SchedulerRepository.getInstance();
Scheduler sched = schedRep.lookup(getSchedulerName());
if (sched != null) {
if (sched.isShutdown()) {
schedRep.remove(getSchedulerName());
} else {
return sched;
}
}
// 第二步:初始化,生成scheduler
sched = instantiate();
return sched;
}
A total of two logics are completed here: load configuration and generate scheduler, and then enter the core method. instantiate()
There are many logics in it. The core operation is to initialize the objects required for various scheduling, such as thread pool, JobStore, etc., and finally create the above Put the object into QuartzSchedulerResources and put the thread pool up, which is equivalent to the resource storage place of QuartzScheduler. The relevant code of the method is as follows:
private Scheduler instantiate() throws SchedulerException{
......
// 要初始化的对象
JobStore js = null;
ThreadPool tp = null;
QuartzScheduler qs = null;
DBConnectionManager dbMgr = null;
String instanceIdGeneratorClass = null;
Properties tProps = null;
String userTXLocation = null;
boolean wrapJobInTx = false;
boolean autoId = false;
long idleWaitTime = -1;
long dbFailureRetry = 15000L; // 15 secs
String classLoadHelperClass;
String jobFactoryClass;
ThreadExecutor threadExecutor;
.....
QuartzSchedulerResources rsrcs = new QuartzSchedulerResources();
rsrcs.setName(schedName);
rsrcs.setThreadName(threadName);
rsrcs.setInstanceId(schedInstId);
rsrcs.setJobRunShellFactory(jrsf);
rsrcs.setMakeSchedulerThreadDaemon(makeSchedulerThreadDaemon);
rsrcs.setThreadsInheritInitializersClassLoadContext(threadsInheritInitalizersClassLoader);
rsrcs.setRunUpdateCheck(!skipUpdateCheck);
rsrcs.setBatchTimeWindow(batchTimeWindow);
rsrcs.setMaxBatchSize(maxBatchSize);
rsrcs.setInterruptJobsOnShutdown(interruptJobsOnShutdown);
rsrcs.setInterruptJobsOnShutdownWithWait(interruptJobsOnShutdownWithWait);
rsrcs.setJMXExport(jmxExport);
rsrcs.setJMXObjectName(jmxObjectName);
//这个线程执行者用于后面启动调度线程
rsrcs.setThreadExecutor(threadExecutor);
threadExecutor.initialize();
rsrcs.setThreadPool(tp);
if (tp instanceof SimpleThreadPool) {
if (threadsInheritInitalizersClassLoader)
((SimpleThreadPool) tp).setThreadsInheritContextClassLoaderOfInitializingThread(
threadsInheritInitalizersClassLoader);
}
//执行线程池启动
tp.initialize();
tpInited = true;
rsrcs.setJobStore(js);
// add plugins
for (int i = 0; i < plugins.length; i++) {
rsrcs.addSchedulerPlugin(plugins[i]);
}
//调度线程在构造方法里面启动的
qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);
}
The scheduler is initialized after the above scheduler, and then you can define the Job and Trigger, and then scheduler.scheduleJob(jb, t)
register the tasks and triggers.
Register tasks and triggers
scheduler.scheduleJob(jb, t)
EnterStdScheduler#scheduleJob(JobDetail jobDetail, Trigger trigger)
public Date scheduleJob(JobDetail jobDetail, Trigger trigger)
throws SchedulerException {
return sched.scheduleJob(jobDetail, trigger);
}
The object here sched
is QuartzScheduler
to enter sched.scheduleJob(jobDetail, trigger)
, here is the core logic of registration tasks and scheduled tasks.
public Date scheduleJob(JobDetail jobDetail, Trigger trigger) throws SchedulerException {
.....
//核心代码:存储给定的org.quartz.JobDetail和org.quartz.Trigger。
resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
notifySchedulerListenersJobAdded(jobDetail);
notifySchedulerThread(trigger.getNextFireTime().getTime());
notifySchedulerListenersSchduled(trigger);
return ft;
}
Here resources
is to initialize various objects when creating the scheduler above and then put them in the resource management office QuartzSchedulerResources
, which contains the JobStore object, and then save tasks and triggers through this object. As for the details of the storage logic, I will not elaborate here, please do it yourself Check, anyway, the context of the core logic is right here.
Start the scheduler to execute tasks
Quartz uses a thread to continuously poll to find the task to be executed next time, and hands the task to the thread pool for execution. There are two roles involved here: scheduling thread and execution thread pool.
scheduler.start();
scheduler.start()
Call QuartzScheduler.start()
, the start of Quartz needs to call the start() method to start the thread. The start of the thread in the thread is to call the start() method, but the operation of actually executing the thread task is in run()
QuartzScheduler.start()
code show as below:
public void start() throws SchedulerException {
if (shuttingDown|| closed) {
throw new SchedulerException(
"The Scheduler cannot be restarted after shutdown() has been called.");
}
notifySchedulerListenersStarting();
if (initialStart == null) {//初始化标识为null,进行初始化操作
initialStart = new Date();
this.resources.getJobStore().schedulerStarted();//1 主要分析的地方
startPlugins();
} else {
resources.getJobStore().schedulerResumed();//2 如果已经初始化过,则恢复jobStore
}
schedThread.togglePause(false);//3 唤醒所有等待的线程
getLog().info(
"Scheduler " + resources.getUniqueIdentifier() + " started.");
notifySchedulerListenersStarted();
}
this.resources.getJobStore().schedulerStarted()
; The main analysis is actually the start QuartzSchedulerResources
of the call.JobStore
Finally, QuartzSchedulerThread.run() is mainly to obtain the Trigger that needs to be executed when there are available threads and trigger the scheduling of tasks!
Look at the run () method of the thread QuartzSchedulerThread in a while (true) manner, and continuously obtain the trigger set to be triggered next time from the jobStore, and put the task in the thread pool for execution. This is also the way Quartz implements the periodic execution of tasks The core, please see the specific analysis: https://my.oschina.net/chengxiaoyuan/blog/674603
Huawei officially released HarmonyOS 4 miniblink version 108 successfully compiled. Bram Moolenaar, the father of Vim, the world's smallest Chromium kernel, passed away due to illness . ChromeOS split the browser and operating system into an independent Bilibili (Bilibili) station and collapsed again . HarmonyOS NEXT: use full The self-developed kernel Nim v2.0 is officially released, and the imperative programming language Visual Studio Code 1.81 is released. The monthly production capacity of Raspberry Pi has reached 1 million units. Babitang launched the first retro mechanical keyboard