4、Quartz 实战:Spring Boot 集成与动态任务管理

Quartz 这东西,说白了就是 Java 世界里最老牌的任务调度框架。我最早接触它是在 2012 年,那时候还没 Spring Boot,配置全靠 XML,写起来挺痛苦的。现在好了,Spring Boot 把 Quartz 集成得相当丝滑,但很多细节还是得自己注意。

这一章,咱们直接上手。我会带着你从零搭建一个 Spring Boot + Quartz 项目,然后实现任务的动态创建、暂停、恢复,最后聊聊集群部署。嗯,都是我在生产环境里踩过的坑。

本章核心知识点

  • Spring Boot 自动配置 Quartz 的原理
  • JobDetail、Trigger、Scheduler 三者的关系
  • 动态管理任务的 API 使用
  • 集群模式下数据库锁的机制
Quartz 实战知识体系 Spring Boot + Quartz 自动配置原理 JobDetail & Trigger 动态管理 API 集群部署方案 DataSource 配置 Cron 表达式 暂停/恢复/删除 数据库锁机制 核心:Quartz 通过 JobStore 持久化任务状态,集群模式下依赖数据库行级锁保证任务不重复执行 动态管理本质是操作 Scheduler 接口,将 JobDetail 和 Trigger 注册到 JobStore

4.1 Spring Boot 集成 Quartz

Spring Boot 从 2.0 开始就内置了对 Quartz 的支持。你只需要引入一个 starter,然后配置一下数据源,基本就能跑了。我个人习惯用 spring-boot-starter-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>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

然后配置 application.yml:

spring:
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: always
    properties:
      org:
        quartz:
          scheduler:
            instanceName: MyScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10

小提示:initialize-schema: always 会在第一次启动时自动创建 Quartz 需要的 11 张表。生产环境建议改成 never,手动执行建表脚本。

写一个简单的 Job 类:

public class SimpleJob implements Job {
    @Override
    public void execute(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("任务 " + jobName + " 执行时间: " + LocalDateTime.now());
    }
}

再配一个 Trigger:

@Configuration
public class QuartzConfig {
    @Bean
    public JobDetail simpleJobDetail() {
        return JobBuilder.newJob(SimpleJob.class)
                .withIdentity("simpleJob", "group1")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger simpleTrigger() {
        return TriggerBuilder.newTrigger()
                .forJob(simpleJobDetail())
                .withIdentity("simpleTrigger", "group1")
                .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
                .build();
    }
}

启动项目,每 5 秒就会打印一次日志。嗯,就这么简单。但这里有个坑——Job 类必须有无参构造器,Quartz 是通过反射创建实例的。我曾经见过有人给 Job 加了 @Autowired 构造器注入,结果启动就报错。

4.2 动态创建、暂停、恢复任务

静态配置只适合固定任务。真实项目里,任务往往是用户在前端页面创建的。比如运营后台要配置一个定时推送通知,总不能每次改代码重启吧?

这时候就需要动态管理了。核心是注入 Scheduler 对象,然后调用它的 API。

4.2.1 动态创建任务

@Service
public class DynamicJobService {
    @Autowired
    private Scheduler scheduler;

    public void createJob(String jobName, String group, String cron) throws SchedulerException {
        JobDetail jobDetail = JobBuilder.newJob(DynamicJob.class)
                .withIdentity(jobName, group)
                .storeDurably()
                .build();

        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName + "Trigger", group)
                .withSchedule(CronScheduleBuilder.cronSchedule(cron))
                .build();

        scheduler.scheduleJob(jobDetail, trigger);
    }
}

这里要注意,DynamicJob 是一个通用的 Job 实现类,它从 JobDataMap 里读取具体业务参数:

public class DynamicJob implements Job {
    @Override
    public void execute(JobExecutionContext context) {
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String businessKey = dataMap.getString("businessKey");
        // 根据 businessKey 执行具体业务逻辑
        System.out.println("执行业务: " + businessKey);
    }
}

4.2.2 暂停与恢复

暂停任务:

public void pauseJob(String jobName, String group) throws SchedulerException {
    JobKey jobKey = new JobKey(jobName, group);
    scheduler.pauseJob(jobKey);
}

恢复任务:

public void resumeJob(String jobName, String group) throws SchedulerException {
    JobKey jobKey = new JobKey(jobName, group);
    scheduler.resumeJob(jobKey);
}

删除任务:

public void deleteJob(String jobName, String group) throws SchedulerException {
    JobKey jobKey = new JobKey(jobName, group);
    scheduler.deleteJob(jobKey);
}

注意:删除任务前,最好先暂停它。否则如果任务正在执行,删除操作可能会抛异常。我曾经在生产环境遇到过这个问题,当时任务执行时间较长,删除时直接报了 JobPersistenceException。

4.3 集群部署方案

单机 Quartz 有个致命问题——如果服务器挂了,所有任务都停了。生产环境必须上集群。

Quartz 的集群方案其实不复杂,核心思想是:多个节点共享同一个数据库,通过数据库行级锁来竞争任务执行权。

配置集群模式,只需要改两个地方:

spring:
  quartz:
    properties:
      org:
        quartz:
          jobStore:
            isClustered: true
            clusterCheckinInterval: 20000
          scheduler:
            instanceId: AUTO

解释一下这几个参数:

参数 说明 建议值
isClustered 开启集群模式 true
clusterCheckinInterval 节点心跳检测间隔(毫秒) 20000
instanceId 节点唯一标识,AUTO 自动生成 AUTO

集群模式下,Quartz 会使用 QRTZ_LOCKS 表来加锁。当一个节点要执行任务时,它会先尝试获取锁。获取成功的节点执行,其他节点等待。

集群部署的关键点

  • 所有节点必须使用同一个数据库
  • 各节点的系统时间必须同步(建议用 NTP)
  • 每个节点的 instanceId 必须唯一
  • 任务默认不会重复执行,因为锁机制保证了互斥

你可能会问:如果一个节点挂了,任务会不会丢?不会。Quartz 的集群容错机制是这样的:每个节点会定期向数据库写入心跳。如果某个节点超过 clusterCheckinInterval 的 2 倍时间没有更新心跳,其他节点就会认为它挂了,然后接管它的任务。

我曾经遇到过一个坑:时间不同步导致任务被重复执行。当时有两台服务器,一台快了 30 秒,结果两台都认为自己该执行任务,锁机制没拦住。从那以后,我每次部署集群都会检查 NTP 配置。

4.4 实战中的避坑指南

最后,分享几个我踩过的坑:

  1. Job 类不要用 @Scope("prototype")。Quartz 自己管理 Job 实例的生命周期,Spring 的 scope 不生效。
  2. JobDataMap 只支持基本类型和 String。如果你存了一个复杂对象,反序列化时会报错。我一般只存业务主键,具体数据从数据库查。
  3. 集群模式下,Cron 表达式不要写太密集。比如每秒执行一次,锁竞争会很激烈,反而降低性能。
  4. 数据库表要定期清理。Quartz 的 QRTZ_TRIGGERS 和 QRTZ_FIRED_TRIGGERS 表会积累大量历史数据,建议写个定时任务清理 7 天前的记录。

我的建议:如果项目刚起步,任务量不大,用单机模式就够了。等业务量上来,再切换到集群模式。Quartz 的集群配置是运行时生效的,不需要改代码,只需要改配置文件和数据库连接。