Implementing Distributed Scheduled Tasks with Quartz in Spring Boot

This tutorial explains how to build a highly available, horizontally scalable distributed scheduled‑task system using Quartz in a Spring Boot application, covering application scenarios, Maven dependencies, database initialization, configuration properties, job and scheduler code, and verification of failover behavior.

DataFunSummit
DataFunSummit
DataFunSummit
Implementing Distributed Scheduled Tasks with Quartz in Spring Boot

This article demonstrates how to implement a distributed scheduled‑task solution with Quartz in a Spring Boot project, targeting scenarios that require high availability and horizontal scalability beyond a single server.

Application scenarios : While simple tasks can run on a single node, growing workloads in e‑commerce, finance, and other domains need a clustered scheduler that provides failover and load‑balancing.

1. Add Quartz dependencies (Maven):

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

<!-- MySQL driver -->
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- JPA for job persistence -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

These dependencies provide Quartz’s clustering support and the JDBC job store needed for persisting jobs in MySQL.

2. Initialize Quartz tables using the script supplied with Quartz (tables‑mysql.sql). A shortened excerpt of the DDL is shown below:

DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
... (other DROP statements) ...
CREATE TABLE QRTZ_JOB_DETAILS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    JOB_CLASS_NAME   VARCHAR(250) NOT NULL,
    IS_DURABLE VARCHAR(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
... (other CREATE TABLE statements for QRTZ_TRIGGERS, QRTZ_SIMPLE_TRIGGERS, etc.) ...

Executing the script creates the required tables in the quartz_jobs database.

3. Configure datasource and Quartz properties in application.properties:

# Quartz datasource
spring.datasource.url=jdbc:mysql://localhost:3306/quartz_jobs?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# Quartz job store settings
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.clusterCheckinInterval=5000
org.quartz.jobStore.misfireThreshold=60000
org.quartz.jobStore.txIsolationLevelReadCommitted=true
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.scheduler.instanceName=ClusterQuartz
org.quartz.scheduler.instanceId=AUTO
org.quartz.scheduler.rmi.export=false
org.quartz.scheduler.rmi.proxy=false
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false

These settings enable Quartz to use the MySQL job store and operate in clustered mode.

4. Define a Quartz job with persistence and non‑concurrent execution annotations:

// Persist job data after execution
@PersistJobDataAfterExecution
// Disallow concurrent execution of the same job
@DisallowConcurrentExecution
public class QuartzJob extends QuartzJobBean {
    private static final Logger log = LoggerFactory.getLogger(QuartzJob.class);

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        String taskName = context.getJobDetail().getJobDataMap().getString("name");
        log.info("---> Quartz job, time:{" + new Date() + "} ,name:{" + taskName + "}<---");
    }
}

5. Scheduler configuration creates the Spring‑managed Scheduler and a thread pool:

@Configuration
public class SchedulerConfig {
    @Autowired
    private DataSource dataSource;

    @Bean
    public Scheduler scheduler() throws Exception {
        return schedulerFactoryBean().getScheduler();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setSchedulerName("Cluster_Scheduler");
        factory.setDataSource(dataSource);
        factory.setApplicationContextSchedulerContextKey("applicationContext");
        factory.setTaskExecutor(schedulerThreadPool());
        factory.setStartupDelay(10);
        return factory;
    }

    @Bean
    public Executor schedulerThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
        executor.setQueueCapacity(Runtime.getRuntime().availableProcessors());
        return executor;
    }
}

6. Trigger jobs at application start using a CommandLineRunner implementation that creates jobs only if they do not already exist in the cluster:

@Component
public class JobStartupRunner implements CommandLineRunner {
    @Autowired
    SchedulerConfig schedulerConfig;
    private static final String TRIGGER_GROUP_NAME = "test_trigger";
    private static final String JOB_GROUP_NAME = "test_job";

    @Override
    public void run(String... args) throws Exception {
        Scheduler scheduler = schedulerConfig.scheduler();
        // Job 1
        TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", TRIGGER_GROUP_NAME);
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        if (trigger == null) {
            JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
                .withIdentity("job1", JOB_GROUP_NAME)
                .usingJobData("name", "weiz QuartzJob")
                .build();
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
            trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", TRIGGER_GROUP_NAME)
                .withSchedule(scheduleBuilder)
                .build();
            scheduler.scheduleJob(jobDetail, trigger);
            System.out.println("Quartz created job: " + jobDetail.getKey());
        }
        // Job 2 – similar logic omitted for brevity
        scheduler.start();
    }
}

Running the application starts the scheduler; each node in the cluster checks the database, creates missing jobs, and then begins execution.

7. Validation and failover testing : After launching two instances, logs show that QuartzJob and QuartzJob2 alternate between the two processes, confirming that only one node executes a given job at a time. Stopping one instance causes the remaining node to pick up all jobs, demonstrating automatic failover.

Conclusion : By integrating Quartz with Spring Boot, configuring a JDBC job store, and enabling clustering, developers obtain a robust distributed scheduling component that ensures high availability, load balancing, and seamless recovery when individual nodes fail.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaDistributed SchedulingBackend DevelopmentSpring BootQuartz
DataFunSummit
Written by

DataFunSummit

Official account of the DataFun community, dedicated to sharing big data and AI industry summit news and speaker talks, with regular downloadable resource packs.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.