SpringBoot 整合 Quartz 实现定时任务持久化实战

举报
JiHua 发表于 2026/11/07 11:32:47 2026/11/07
【摘要】 在项目开发中,简单定时任务可以使用`@Scheduled`注解快速实现,但它存在无法动态修改任务、重启丢失任务等短板。本文实战演示SpringBoot整合Quartz定时任务框架,将任务信息持久化到MySQL,支持动态新增、暂停、恢复、删除定时任务,同时讲解Quartz核心概念、表结构说明、避坑点,适合后台管理系统的定时调度场景。

前言

日常开发里,@Scheduled使用简单,代码写死Cron表达式,打包部署后想要修改执行时间,只能改代码重新发布,灵活性很差。
Quartz是成熟的开源定时调度框架,支持任务持久化、集群调度、动态管理任务。当多个服务实例部署时,持久化模式下Quartz会通过数据库锁保证同一个任务同一时间只执行一次,避免重复执行。

本文使用SpringBoot 2.7.x + Quartz + MySQL,实现可动态管理的定时任务,任务和触发器全部落库。

一、环境依赖

<!-- SpringBoot Quartz 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- mybatis-plus 可选,用来操作quartz业务表 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

二、Quartz数据库初始化脚本

Quartz官方提供11张表,用于存储任务、触发器、调度状态、锁信息。

注意:表前缀默认QRTZ_,需要和配置文件保持一致。

# 这里只放核心表说明,完整脚本可以在quartz源码包中找到 schema_mysql.sql
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)
);

CREATE TABLE QRTZ_TRIGGERS (
  SCHED_NAME VARCHAR(120) NOT NULL,
  TRIGGER_NAME VARCHAR(200) NOT NULL,
  TRIGGER_GROUP VARCHAR(200) NOT NULL,
  JOB_NAME VARCHAR(200) NOT NULL,
  JOB_GROUP VARCHAR(200) NOT NULL,
  DESCRIPTION VARCHAR(250) NULL,
  NEXT_FIRE_TIME BIGINT(13) NULL,
  PREV_FIRE_TIME BIGINT(13) NULL,
  PRIORITY INTEGER NULL,
  TRIGGER_STATE VARCHAR(16) NOT NULL,
  TRIGGER_TYPE VARCHAR(8) NOT NULL,
  START_TIME BIGINT(13) NOT NULL,
  END_TIME BIGINT(13) NULL,
  CALENDAR_NAME VARCHAR(200) NULL,
  MISFIRE_INSTR SMALLINT(2) NULL,
  JOB_DATA BLOB NULL,
  PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
  FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);

三、application.yml 配置

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/quartz_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: never
    properties:
      org.quartz:
        scheduler:
          instanceName: quartzScheduler
          instanceId: AUTO
        jobStore:
          class: org.quartz.impl.jdbcjobstore.JobStoreTX
          driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
          tablePrefix: QRTZ_
          isClustered: true
          clusterCheckinInterval: 10000
        threadPool:
          class: org.quartz.simpl.SimpleThreadPool
          threadCount: 10
          threadPriority: 5

配置说明:

  • initialize-schema: never:不自动建表,我们手动执行SQL初始化;
  • isClustered: true:开启集群模式,多实例部署时自动抢占锁;
  • threadCount:调度线程数量,根据任务并发量调整。

四、自定义Job任务类

所有任务都需要实现Job接口,重写execute方法,任务执行逻辑写在这里。

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.time.LocalDateTime;

public class DemoTaskJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取任务参数
        String taskParam = context.getJobDetail().getJobDataMap().getString("taskParam");
        System.out.println("【定时任务执行】时间:" + LocalDateTime.now() + ",参数:" + taskParam);
    }
}

五、调度工具类(新增、暂停、恢复、删除任务)

import org.quartz.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

@Component
public class QuartzTaskUtil {

    @Resource
    private Scheduler scheduler;

    /**
     * 添加定时任务
     */
    public void addJob(String jobName, String jobGroup, String cron, String param, Class<? extends Job> jobCls) throws SchedulerException {
        JobDetail jobDetail = JobBuilder.newJob(jobCls)
                .withIdentity(jobName, jobGroup)
                .storeDurably()
                .build();
        jobDetail.getJobDataMap().put("taskParam", param);

        CronScheduleBuilder cronSchedule = CronScheduleBuilder.cronSchedule(cron);
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName, jobGroup)
                .withSchedule(cronSchedule)
                .build();

        scheduler.scheduleJob(jobDetail, trigger);
    }

    /**
     * 暂停任务
     */
    public void pauseJob(String jobName, String jobGroup) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        scheduler.pauseJob(jobKey);
    }

    /**
     * 恢复任务
     */
    public void resumeJob(String jobName, String jobGroup) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        scheduler.resumeJob(jobKey);
    }

    /**
     * 删除任务
     */
    public void deleteJob(String jobName, String jobGroup) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        scheduler.deleteJob(jobKey);
    }

    /**
     * 修改cron表达式
     */
    public void updateCron(String jobName, String jobGroup, String newCron) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(newCron);
        trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
        scheduler.rescheduleJob(triggerKey, trigger);
    }
}

六、测试Controller

import org.quartz.SchedulerException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
@RequestMapping("/quartz")
public class QuartzController {

    @Resource
    private QuartzTaskUtil quartzTaskUtil;

    @GetMapping("/add")
    public String addTask(@RequestParam String cron, @RequestParam String param) throws SchedulerException {
        quartzTaskUtil.addJob("demo-task-01", "demo-group", cron, param, DemoTaskJob.class);
        return "任务添加成功";
    }

    @GetMapping("/pause")
    public String pauseTask() throws SchedulerException {
        quartzTaskUtil.pauseJob("demo-task-01", "demo-group");
        return "任务暂停成功";
    }

    @GetMapping("/resume")
    public String resumeTask() throws SchedulerException {
        quartzTaskUtil.resumeJob("demo-task-01", "demo-group");
        return "任务恢复成功";
    }

    @GetMapping("/update")
    public String updateCron(@RequestParam String cron) throws SchedulerException {
        quartzTaskUtil.updateCron("demo-task-01", "demo-group", cron);
        return "Cron修改成功";
    }
}

七、常见坑点总结

  1. Cron表达式错误:Quartz的cron是7位(秒 分 时 日 月 周 年),和Linux crontab不一样,很容易写错;
  2. 任务并发执行:同一个任务上一次没跑完,下一次触发又进来,给Job类加上注解@DisallowConcurrentExecution禁止并发;
  3. Misfire错过策略:服务器宕机期间错过的任务,恢复后怎么执行,可以在Trigger配置Misfire策略;
  4. 集群锁失效:数据库连接不通、时区不一致,会导致多实例同时执行任务;
  5. 事务问题:Quartz默认的JobStoreTX是独立事务,业务代码异常需要自己处理回滚。

八、适用场景与对比

方案 优点 缺点
@Scheduled 简单、零依赖 不可动态修改、不支持集群
Quartz 持久化、动态调度、集群 配置复杂,需要维护11张表
XXL-Job 可视化后台、告警、分片 需要额外部署调度中心

对于中小型后台管理系统,如果不想额外部署中间件,Quartz持久化方案是很合适的选择。

结语

通过Quartz持久化方案,我们可以在后台页面做一个任务管理面板,运营人员直接在页面新增、修改定时任务,不用改代码重启服务。
在实际生产中,还可以扩展:任务执行日志表、执行失败告警、任务超时监控等功能。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。