Quartz
之前在项目中用定时任务都是直接用的Spring的@Scheduled注解,因为遇到的业务都比较简单,比如定时更新榜单,支付功能中的定时查单,最近在写一个课堂助手的项目,要实现定时任务的业务对任务有增删改查的需求,@Scheduled已经无法满足了,so,我选择了Quartz
Quartz是OpenSymphony开源的一个项目,是一个由Java编写的开源作业调度框架。
- 支持分布式高可用
- 支持持久化,支持调度数据的多种存储方式
- 支持多任务调度和管理
存储方式
- RAMJobStore
- 不要外部数据库,配置容易,运行快
- 调度程序信息存在内存中,当应用程序停止运行时,所有调度信息将丢失
- 而且因为存在内存中,Job和Trigger的数量将会受到限制
- JDBCJobStore
- 支持集群
- 所有任务信息都会保存在数据库中,不会因为程序停止运行丢失
- 运行速度取决于数据库的速度
组件
Quartz的组成
JobDetail
Job相当于是线程池中的task,是定时任务真正业务逻辑的部分,Job需要封装成JobDetail,一个Job可以对应多JobDetail
通过JobBuilder创建
Trigger
触发器,定义触发时间,常用的有以下两种
- SimleTrigger:用于实现简单的定时,比如定频率的执行某个任务
- CronTrigger:配合Cron表达式使用,可以实现相对复杂的业务,比如到某个具体日期执行,每个月几号执行等
通过TriggerBuilder创建
Scheduler
调度器,帮我们把JobDetail和Trigger绑定在一起,按照Trigger中定义的触发时间去触发JobDetail中的Job
使用SchedulerFactory创建
DirectSchedulerFactory
:需要在代码中定义一些属性,不常用StdSchedulerFactory
:从配置文件中获取配置,推荐使用
//调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 这里简单获取了一个默认的,可以在配置文件中配置属性并获取
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
任务每次执行Scheduler都会根据JobDetail创建一个新的Job实例,让任务并发执行,如果我们不想要这种特性,想要前一个任务执行完了才会执行下一个,可以使用@DisallowConcurrentExecution
注解关闭
由于Scheduler每次执行都会根据JobDetail创建一个新的Job实例,jobDataMap属于JobDetail,那么每次也是一个新的,我们可以使用@PersistJobDataAfterExecution
来让JobDataMap持久化
JobDataMap
我们可以看到JobDetail和Trigger中都有JobDataMap,jobDataMap可以用于在启动这个定时器时,往任务中传递一些参数
任务类获取参数的方式有两种
- 通过获取jobDataMap再获取对应键值
- 在任务类中定义相关字段,设置好set方法,框架会自动往里面设置值,如果JobDetail和Trigger里设置相同名称的话,JobDetail的会被覆盖掉
JobDetail jobDetail = JobBuilder.newJob(MyJob.class).withIdentity("job1","group1").usingJobData("job","gwj's job") // 用来设置JobDetail中的JobDataMap中的值.usingJobData("name","jobdetail").usingJobData("tong","jobde").build();Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1","triggerGroup1").usingJobData("trigger","my trigger") // 用来设置Trigger中的JobDataMap中的值.usingJobData("name","trigger").usingJobData("tong","triggde").startNow().withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever()).build();
任务类
@Data
public class MyJob implements Job {private String name;@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {JobDataMap jobDetailMap = jobExecutionContext.getJobDetail().getJobDataMap(); // 获取JobDetail的JobDataMapJobDataMap triggerMap = jobExecutionContext.getTrigger().getJobDataMap(); // 获取Trigger的JobDataMapJobDataMap mergeMap = jobExecutionContext.getMergedJobDataMap(); // 获取混合JobDataMapSystem.out.println(mergeMap.get("job")); // gwj's jobSystem.out.println(mergeMap.get("trigger")); // my triggerSystem.out.println(mergeMap.get("tong")); // triggdeSystem.out.println(name); // trigger// 具体任务流程……}
}
整合SpringBoot
导入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
配置文件
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/test_dbusername: rootpassword: 111111type: com.alibaba.druid.pool.DruidDataSourceservlet:multipart:max-file-size: 2048MBmax-request-size: 2048MB# 定时任务配置quartz:# 数据库方式job-store-type: jdbcjdbc:initialize-schema: always # 数据库架构初始化模式# never:从不 always:每次都清空数据库初始化 embedded:只初始化内存数据库(默认)# quartz 相关属性配置properties:org:quartz:scheduler:instanceName: demoSchedulerinstanceId: AUTOjobStore:class: org.springframework.scheduling.quartz.LocalDataSourceJobStoredriverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegatetablePrefix: QRTZ_isClustered: trueclusterCheckinInterval: 10000useProperties: falsethreadPool:class: org.quartz.simpl.SimpleThreadPoolthreadCount: 10threadPriority: 5threadsInheritContextClassLoaderOfInitializingThread: true
创建表
Quartz支持对任务进行crud,可以使用mysql存储记录我们的任务
方式一
配置文件中配置initialize-schema: always
,项目启动后我们就可以看到数据库中自动生成了11张表,后面再删掉或者改成never
就行
方式二
可以在Quartz的jar包中找到对应数据库的SQL脚本,拷贝出来执行即可
表说明
表名 | 说明 |
---|---|
qrtz_blob_triggers | 以Blob 类型存储的触发器 |
qrtz calendars | 存放日历信息,quartz可配置一个日历来指定一个时间范围 |
qrtz_cron triggers | 存放cron类型的触发器 |
qrtz fired triggers | 存放已触发的触发器 |
qrtz job _details | 存放一个jobDetail信息 |
qrtz job listeners | job监听器 |
qrtz_locks | 存储程序的悲观锁的信息(假如使用了悲观锁) |
qrtz_paused trigger_graps | 存放暂停掉的触发器 |
qrtz scheduler state | 调度器状态 |
qrtz simple triggers | 简单触发器的信息 |
qrtz_trigger_listeners | 触发器监听器 |
编写业务类
示例
/*** 任务业务类,用于动态处理任务信息* @author gwj* @date 2024/11/29 下午2:17*/
public interface JobService {/*** 任务数据*/String TASK_DATA = "taskData";/*** 添加定时任务* @param jobClass* @param jobName* @param cron* @param data*/void addCronJob(Class jobClass, String jobName, String cron, String data);/*** 添加立即执行的任务* @param jobClass* @param jobName* @param data*/void addCronJob(Class jobClass, String jobName, String data);/*** 暂停任务* @param jobName* @param jobGroup*/void pauseJob(String jobName, String jobGroup);/*** 恢复任务* @param triggerName* @param triggerGroup*/void resumeJob(String triggerName, String triggerGroup);/*** 删除job* @param jobName* @param jobGroup*/void deleteJob(String jobName, String jobGroup);
}
/*** @author gwj*/
@Slf4j
@Service
public class JobServiceImpl implements JobService {/*** Quartz定时任务核心的功能实现类*/@Autowiredprivate Scheduler scheduler;@Overridepublic void addCronJob(Class jobClass, String jobName, String cron, String data) {String jobGroup = JobGroup.SYSTEM;// 自动命名if(StringUtils.isEmpty(jobName)){jobName = jobClass.getSimpleName().toUpperCase() + "_"+ IdWorker.getIdStr();}try {JobKey jobKey = JobKey.jobKey(jobName, jobGroup);JobDetail jobDetail = scheduler.getJobDetail(jobKey);if (jobDetail != null) {log.info("++++++++++任务:{} 已存在", jobName);this.deleteJob(jobName, jobGroup);}log.info("++++++++++构建任务:{},{},{},{},{} ", jobClass.toString(), jobName, jobGroup, cron, data);//构建job信息jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).build();//用JopDataMap来传递数据jobDetail.getJobDataMap().put(TASK_DATA, data);//按新的cronExpression表达式构建一个新的triggerTrigger trigger = null;// 有表达式的按表达式if(!StringUtils.isEmpty(cron)){log.info("+++++表达式执行:"+ JSON.toJSONString(jobDetail));//表达式调度构建器CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).withSchedule(scheduleBuilder).build();}else{// 无表达式则立即执行log.info("+++++立即执行:"+ JSON.toJSONString(jobDetail));trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).startNow().build();}scheduler.scheduleJob(jobDetail, trigger);} catch (Exception e) {e.printStackTrace();}}@Overridepublic void addCronJob(Class jobClass, String jobName, String data) {// 立即执行任务this.addCronJob(jobClass, jobName, null, data);}@Overridepublic void pauseJob(String jobName, String jobGroup) {try {TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);scheduler.pauseTrigger(triggerKey);log.info("++++++++++暂停任务:{}", jobName);} catch (SchedulerException e) {e.printStackTrace();}}@Overridepublic void resumeJob(String jobName, String jobGroup) {try {TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);scheduler.resumeTrigger(triggerKey);log.info("++++++++++重启任务:{}", jobName);} catch (SchedulerException e) {e.printStackTrace();}}@Overridepublic void deleteJob(String jobName, String jobGroup) {try {JobKey jobKey = JobKey.jobKey(jobName,jobGroup);scheduler.deleteJob(jobKey);log.info("++++++++++删除任务:{}", jobKey);} catch (SchedulerException e) {e.printStackTrace();}}
}
测试
/*** @author gwj* @date 2024/11/30 21:18*/
@RestController
@RequestMapping("/test")
public class TestController {@Resourceprivate JobService jobService;/*** 创建一个一分钟后执行的定时任务** @param id* @return*/@PostMappingpublic String addTask(Long id) {String jobName = JobPrefix.BREAK_EXAM + id;Date date = new Date();long l = date.getTime() + 1000 * 60;Date until = new Date(l);jobService.addCronJob(BreakExamJob.class,jobName, CronUtils.dateToCron(until),id+"");return "定时任务创建成功";}
}