SpringBoot 整合 Quartz 实现定时任务持久化实战
【摘要】 在项目开发中,简单定时任务可以使用`@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修改成功";
}
}
七、常见坑点总结
- Cron表达式错误:Quartz的cron是7位(秒 分 时 日 月 周 年),和Linux crontab不一样,很容易写错;
- 任务并发执行:同一个任务上一次没跑完,下一次触发又进来,给Job类加上注解
@DisallowConcurrentExecution禁止并发; - Misfire错过策略:服务器宕机期间错过的任务,恢复后怎么执行,可以在Trigger配置Misfire策略;
- 集群锁失效:数据库连接不通、时区不一致,会导致多实例同时执行任务;
- 事务问题:Quartz默认的JobStoreTX是独立事务,业务代码异常需要自己处理回滚。
八、适用场景与对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| @Scheduled | 简单、零依赖 | 不可动态修改、不支持集群 |
| Quartz | 持久化、动态调度、集群 | 配置复杂,需要维护11张表 |
| XXL-Job | 可视化后台、告警、分片 | 需要额外部署调度中心 |
对于中小型后台管理系统,如果不想额外部署中间件,Quartz持久化方案是很合适的选择。
结语
通过Quartz持久化方案,我们可以在后台页面做一个任务管理面板,运营人员直接在页面新增、修改定时任务,不用改代码重启服务。
在实际生产中,还可以扩展:任务执行日志表、执行失败告警、任务超时监控等功能。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)