Understanding Quartz: The Core of Distributed Task Scheduling
This article introduces Quartz, the Java‑based open‑source scheduler behind many distributed task‑scheduling frameworks, compares its RAMJobStore and JDBCJobStore options, shows step‑by‑step Spring Boot integration with code samples, and explains the internal architecture and lifecycle of the scheduler.
1. Introduction
Our company recently deployed the distributed task scheduling framework XXL‑JOB, which is built on the open‑source Quartz scheduler. Quartz, written entirely in Java, is a de‑facto standard for Java‑based timed jobs.
1.1 What is Quartz
Quartz is an open‑source job‑scheduling project from the OpenSymphony community. It provides a “task progress manager” that triggers execution of other components at pre‑defined times, offering richer features than java.util.Timer.
Powerful scheduling capabilities supporting many scheduling methods.
Flexible usage with multiple job‑store options.
Built‑in support for distributed and clustered deployments.
1.2 Storage Options
Quartz supports two main JobStore implementations:
RAMJobStore – No external database required, easy to configure, fast execution, but all scheduling data resides in JVM memory and is lost when the application stops; capacity limited by memory.
JDBCJobStore – Persists job data in a relational database (e.g., MySQL), supports clustering, survives server restarts, and can recover failed executions; performance depends on database connectivity.
To enable clustering, JDBCJobStore must be used. The required tables (e.g., QRTZ_JOB_DETAILS, QRTZ_TRIGGERS, QRTZ_CRON_TRIGGERS, etc.) are created from the provided SQL script.
2. Spring Boot Integration Example
Integrating Quartz with Spring Boot is straightforward. The example uses JDBCJobStore and includes the following Maven dependencies:
<!-- Maven dependencies for spring-boot-starter-quartz, spring-boot-starter-jdbc, mysql-connector-java 5.1.48 -->Data source configuration can share the business database or use a separate one. A multi‑data‑source configuration class is shown, defining beans for the user data source and the Quartz data source.
@Configuration
public class DataSourceConfiguration {
@Primary
@Bean(name = "userDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.user")
public DataSourceProperties userDataSourceProperties() { return new DataSourceProperties(); }
@Primary
@Bean(name = "userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user.hikari")
public DataSource userDataSource() { return createHikariDataSource(userDataSourceProperties()); }
@Bean(name = "quartzDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.quartz")
public DataSourceProperties quartzDataSourceProperties() { return new DataSourceProperties(); }
@Bean(name = "quartzDataSource")
@ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
@QuartzDataSource
public DataSource quartzDataSource() { return createHikariDataSource(quartzDataSourceProperties()); }
private static HikariDataSource createHikariDataSource(DataSourceProperties properties) {
HikariDataSource ds = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(properties.getName())) { ds.setPoolName(properties.getName()); }
return ds;
}
}Application properties configure the scheduler name, job‑store type, auto‑startup, thread‑pool size, and other Quartz settings:
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:
scheduler-name: clusteredScheduler
job-store-type: jdbc
auto-startup: true
startup-delay: 0
wait-for-jobs-to-complete-on-shutdown: true
overwrite-existing-jobs: true
jdbc:
initialize-schema: never
properties:
org:
quartz:
jobStore:
dataSource: quartzDataSource
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 1000
useProperties: false
threadPool:
threadCount: 25
threadPriority: 5
class: org.quartz.simpl.SimpleThreadPoolA simple job Job1 extends QuartzJobBean and logs execution details. The job is declared with @DisallowConcurrentExecution and injected with a Spring service.
@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 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 executed, time: {}, k1={}, count={}, demoService={}]",
fullDateFormat.format(new Date()), k1, count.incrementAndGet(), demoService);
}
}Job and trigger beans are defined using JobBuilder and TriggerBuilder. A SimpleScheduleBuilder runs the job every 30 seconds; a CronScheduleBuilder example runs it every minute.
@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)
.repeatForever();
return TriggerBuilder.newTrigger()
.forJob(job1())
.withIdentity("job1Trigger")
.withSchedule(scheduleBuilder)
.build();
}
@Bean
public Trigger cronJobTrigger() {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0 0/1 * * * ? *");
return TriggerBuilder.newTrigger()
.forJob(job1())
.withIdentity("job1Trigger")
.withSchedule(scheduleBuilder)
.build();
}Running the application produces log entries such as:
2022-09-20 23:17:33.500 INFO 18982 --- [eduler_Worker-2] : [job1 executed, time: 2022-09-20 23:17:33, k1=v1, count=1, demoService=DemoService@3258ebff]The logs show that each execution creates a new Quartz job instance while the injected DemoService remains a singleton Spring bean, demonstrating automatic property injection from JobDataMap.
3. Implementation Principles
Quartz uses a Scheduler to manage JobDetail and Trigger objects, storing them in a JobStore (in‑memory or persistent). A dedicated QuartzSchedulerThread continuously polls the JobStore for the next trigger, handing jobs to a thread pool for execution.
Key classes include: QuartzSchedulerThread – Executes triggers registered with the scheduler. ThreadPool – Provides worker threads for job execution. QuartzSchedulerResources – Holds all resources needed to create a QuartzScheduler. SchedulerFactory – Creates scheduler instances (e.g., StdSchedulerFactory). JobStore – Persists jobs and triggers. Scheduler, Trigger, JobDetail, Job – Core scheduling abstractions.
The article walks through three main steps:
Scheduler initialization – Create a SchedulerFactory, obtain a Scheduler, and invoke instantiate() to set up thread pools, JobStore, and other resources.
Registering jobs and triggers – Call scheduler.scheduleJob(jobDetail, trigger), which stores the job/trigger in the JobStore and notifies listeners.
Starting the scheduler – Invoke scheduler.start(), which starts the QuartzSchedulerThread, wakes waiting threads, and begins processing triggers.
The QuartzSchedulerThread.run() method loops indefinitely, fetching the next fire time from the JobStore and dispatching jobs to the thread pool, which is the core mechanism for periodic task execution.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
