任务调度框架 Quartz
一、定时任务概述
在 Java 中开发定时任务主要有三种解决方案:一是使用JDK 自带的 Timer,二是使用 Spring Task,三是使用第三方组件 ,如Quartz
建议:
-
单体项目架构使用Spring Task
-
分布式项目架构使用Quartz
1. Timer实现任务调度
/** * 基于jdk的任务调度 */ public class JdkTaskDemo { public static void main(String[] args) { //创建定时类 Timer timer = new Timer(); //创建任务类 TimerTask task = new TimerTask() { @Override public void run() { System.out.println("定时任务执行了......"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); } }; //执行定时任务 timer.schedule(task,new Date(),2000); } }
2. Spring-task 实现任务调度
搭建SpringBoot工程,导入spring-boot-starter-web即可,不需导入任何其他依赖
在启动类上使用@EnableScheduling开启任务调度
@SpringBootApplication @EnableScheduling public class TaskStudyApplication { public static void main(String[] args) { SpringApplication.run(TaskStudyApplication.class, args); } }
编写任务类测试
@Component public class SpringTask { @Scheduled(cron = "*/1 * * * * *") public void task1() throws InterruptedException { System.out.println(Thread.currentThread().getName()+":task1--->"+ LocalDateTime.now()); } }
注意:
2.1 Cron表达式
关于 cronExpression 表达式有至少 6 个(也可能是 7 个)由空格分隔的时间元素。从左至右,这些元素的定义如下:
1.秒(0–59)
2.分钟(0–59)
3.小时(0–23)
4.月份中的日期(1–31)
5.月份(1–12 或 JAN–DEC)
6.星期中的日期(1–7 或 SUN–SAT)
0 0 10,14,16 * * ? 每天上午 10 点,下午 2 点和下午 4 点
0 0,15,30,45 * 1-10 * ? 每月前 10 天每隔 15 分钟
30 0 0 1 1 ? 2012 在 2012 年 1 月 1 日午夜过 30 秒时
在线生成cron表达式:
二、Quartz
1. Quartz 介绍
-
持久性作业 - 就是保持调度定时的状态;
-
作业管理 - 对调度作业进行有效的管理;
官方文档:
Quartz的一些主要特性和概念:
-
作业(Job)和触发器(Trigger):
-
Quartz中的基本概念是作业和触发器。作业表示要执行的任务,触发器定义了作业执行的时间规则。
-
触发器可以基于特定的时间规则(例如,每天凌晨执行一次)或特定的条件来触发作业。
-
-
-
Quartz的核心组件是调度器,负责管理所有作业的调度和执行。
-
调度器使用作业和触发器来配置和执行任务。
-
-
持久性和集群支持:
-
Quartz支持持久性存储,可以将作业和触发器的配置信息存储在数据库中,确保在应用程序重启后作业调度信息不丢失。
-
还支持集群模式,在集群中多个调度器实例可以协同工作,提高可用性和扩展性。
-
-
监听器(Listener):
-
Quartz提供了监听器机制,允许用户在作业调度的不同阶段附加监听器来执行特定的逻辑,如在作业执行前后执行特定操作。
-
-
灵活的调度配置:
-
Quartz提供了丰富的配置选项,允许对作业和触发器进行灵活的配置,包括执行时间、执行频率、执行策略等。
-
-
异常处理和错过的任务处理:
-
Quartz提供了对任务执行期间的异常处理机制,还可以配置错过触发器的处理方式。
-
-
基于日历的调度:
-
可以基于特定的日历规则来触发作业,比如排除节假日等。
-
2. JobDetail
JobDetail 的作用是绑定 Job,是一个任务实例,它为 Job 添加了许多扩展参数,用于定义和描述被调度的作业(Job)。
含义 | |
---|---|
name | 任务名称 |
group | 任务分组,默认分组DEFAULT |
jobClass | 要执行的Job实现类 |
jobDataMap |
每次Scheduler
调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()
的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。
JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。
这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。
而
JobDetail & Job
关于JobDataMap
和JobDetail
的关系,下面是一些重要的概念:
-
JobDetail中的JobDataMap:
-
每个
JobDetail
对象都可以关联一个JobDataMap
。这个JobDataMap
用于存储与特定的作业实例相关联的数据。这些数据可以是任意的键值对,用于传递给作业实例的执行上下文信息。
-
-
传递参数给Job:
-
当你调度一个
JobDetail
时,JobDataMap
中的数据会被传递给作业实例。作业实例可以通过这个JobDataMap
获取在调度时设置的参数。这为作业提供了一种方式来接收外部的配置和数据。
-
-
Trigger中的JobDataMap:
-
除了
JobDetail
中的JobDataMap
,Quartz还允许你在Trigger
中使用另一个JobDataMap
。这个Trigger
的JobDataMap
中的数据将与JobDetail
的JobDataMap
合并,作为作业实例的完整上下文。
-
-
动态修改JobDataMap:
-
你可以在运行时动态修改
JobDataMap
中的数据,以更新作业实例的上下文。这使得你可以在不修改JobDetail
或Trigger
-
Trigger
-
在指定时间段内,执行一次任务
最基础的 Trigger 不设置循环,设置开始时间。
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger2","group1") .startNow() .withSchedule( //使用简单触发器 SimpleScheduleBuilder.simpleSchedule(). //3s间隔执行 withIntervalInSeconds(3). //始终执行 repeatForever()) //执行6次 count+1 启动时也会执行一次,所以是6次 //.withRepeatCount(5)) .build();
3.2 CronTrigger
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger2","group1") .startNow() .withSchedule( //使用日历触发器 CronScheduleBuilder.cronSchedule("0/1 * * * * ? ")) .build();
三、SpringBoot整合Quartz
引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency> </dependencies>
配置application.yml
server: port: 80 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai # 定时配置 quartz: # 相关属性配置 properties: org: quartz: # 数据源 dataSource: globalJobDataSource: # URL必须大写 URL: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai driver: com.mysql.cj.jdbc.Driver maxConnections: 5 username: root password: 123456 # 必须指定数据源类型 provider: hikaricp scheduler: instanceName: globalScheduler # 实例id instanceId: AUTO type: com.alibaba.druid.pool.DruidDataSource jobStore: # 数据源 dataSource: globalJobDataSource # JobStoreTX将用于独立环境,提交和回滚都将由这个类处理 class: org.quartz.impl.jdbcjobstore.JobStoreTX # 驱动配置 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 表前缀 tablePrefix: QRTZ_ # 失效阈值(只有配置了这个时间,超时策略根据这个时间才有效) misfireThreshold: 100 # 集群配置 isClustered: true # 线程池配置 threadPool: class: org.quartz.simpl.SimpleThreadPool # 线程数 threadCount: 10 # 优先级 threadPriority: 5
这里面有quartz的数据源,线程池,集群和misfire相关配置,简单配置,更多的配置可以到官网查看。
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/
注意:第一次启动要配置,自动生成表,第二次启动不要配置,否则会覆盖先前生成的表
spring.quartz.jdbc.initialize-schema: always spring.quartz.job-store-type: jdbc
实体类
@Data public class JobInfo { /** * 任务名称 */ private String jobName; /** * 任务组 */ private String jobGroup; /** * 触发器名称 */ private String triggerName; /** * 触发器组 */ private String triggerGroup; /** * cron表达式 */ private String cron; /** * 类名 */ private String className; /** * 状态 */ private String status; /** * 下一次执行时间 */ private String nextTime; /** * 上一次执行时间 */ private String prevTime; /** * 配置信息(data) */ private String config; }
任务类
@DisallowConcurrentExecution @PersistJobDataAfterExecution @Slf4j @Component public class MyTask extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) { System.out.println("TimeEventJob正在执行..." + LocalDateTime.now()); // 执行9秒 try { Thread.sleep(9000); System.out.println("TimeEventJob执行完毕..." + LocalDateTime.now()); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
说明:
Job
接口:
Job
是 Quartz 框架中定义的接口,用于表示要执行的任务。- 它有一个方法
execute(JobExecutionContext context)
,该方法定义了任务执行的逻辑。 - 在实现
Job
接口的类中,你需要实现execute
方法,并在其中编写你的任务逻辑。
QuartzJobBean
类:
QuartzJobBean
是 Spring 对 Quartz 提供的Job
接口的一个实现,它充当了一个适配器的角色。- 它扩展了 Quartz 的
Job
类,并提供了对 Spring 管理 bean 的支持。 QuartzJobBean
提供了一个回调方法executeInternal(JobExecutionContext context)
,它与Job
接口中的execute
方法类似,是实际任务逻辑的执行点。
@DisallowConcurrentExecution
:
-
该注解用于防止同一个 JobDetail(任务细节)实例同时运行多个任务实例。
-
如果一个任务的执行时间超过了其触发器的间隔时间,而另一个触发器试图启动同一个任务,
@DisallowConcurrentExecution
会阻止并发执行,确保前一个任务完成后才能启动下一个。
@PersistJobDataAfterExecution
:
-
该注解用于在每次任务执行后持久化 JobDataMap 中的数据。
-
@Configuration public class JobHandler { @Resource private Scheduler scheduler; /** * 添加任务 */ @SuppressWarnings("unchecked") public void addJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException { Objects.requireNonNull(jobInfo, "任务信息不能为空"); // 生成job key JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup()); // 当前任务不存在才进行添加 if (!scheduler.checkExists(jobKey)) { Class<Job> jobClass = (Class<Job>)Class.forName(jobInfo.getClassName()); // 任务明细 JobDetail jobDetail = JobBuilder .newJob(jobClass) .withIdentity(jobKey) .withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup()) .withDescription(jobInfo.getJobName()) .build(); // 配置信息 jobDetail.getJobDataMap().put("config", jobInfo.getConfig()); // 定义触发器 TriggerKey triggerKey = TriggerKey.triggerKey(jobInfo.getTriggerName(), jobInfo.getTriggerGroup()); // 设置任务的错过机制 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity(triggerKey) .withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getCron()).withMisfireHandlingInstructionDoNothing()) .build(); scheduler.scheduleJob(jobDetail, trigger); } else { throw new SchedulerException(jobInfo.getJobName() + "任务已存在,无需重复添加"); } } /** * 任务暂停 */ public void pauseJob(String jobGroup, String jobName) throws SchedulerException { JobKey jobKey = JobKey.jobKey(jobName, jobGroup); if (scheduler.checkExists(jobKey)) { scheduler.pauseJob(jobKey); } } /** * 继续任务 */ public void continueJob(String jobGroup, String jobName) throws SchedulerException { JobKey jobKey = JobKey.jobKey(jobName, jobGroup); if (scheduler.checkExists(jobKey)) { scheduler.resumeJob(jobKey); } } /** * 删除任务 */ public boolean deleteJob(String jobGroup, String jobName) throws SchedulerException { JobKey jobKey = JobKey.jobKey(jobName, jobGroup); if (scheduler.checkExists(jobKey)) { // 这里还需要先删除trigger相关 //TriggerKey triggerKey = TriggerKey.triggerKey(jobInfo.getTriggerName(), jobInfo.getTriggerGroup()); //scheduler.getTrigger() //scheduler.rescheduleJob() return scheduler.deleteJob(jobKey); } return false; } /** * 获取任务信息 */ public JobInfo getJobInfo(String jobGroup, String jobName) throws SchedulerException { JobKey jobKey = JobKey.jobKey(jobName, jobGroup); if (!scheduler.checkExists(jobKey)) { return null; } List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey); if (Objects.isNull(triggers)) { throw new SchedulerException("未获取到触发器信息"); } TriggerKey triggerKey = triggers.get(0).getKey(); Trigger.TriggerState triggerState = scheduler.getTriggerState(triggerKey); JobDetail jobDetail = scheduler.getJobDetail(jobKey); JobInfo jobInfo = new JobInfo(); jobInfo.setJobName(jobGroup); jobInfo.setJobGroup(jobName); jobInfo.setTriggerName(triggerKey.getName()); jobInfo.setTriggerGroup(triggerKey.getGroup()); jobInfo.setClassName(jobDetail.getJobClass().getName()); jobInfo.setStatus(triggerState.toString()); if (Objects.nonNull(jobDetail.getJobDataMap())) { jobInfo.setConfig(JSONObject.toJSONString(jobDetail.getJobDataMap())); } CronTrigger theTrigger = (CronTrigger) triggers.get(0); jobInfo.setCron(theTrigger.getCronExpression()); return jobInfo; } }
Controller
@RestController @RequestMapping("/job") public class QuartzController { @Resource private JobHandler jobHandler; @Resource private Scheduler scheduler; /** * 查询所有的任务 */ @RequestMapping("/all") public List<JobInfo> list() throws SchedulerException { List<JobInfo> jobInfos = new ArrayList<>(); List<String> triggerGroupNames = scheduler.getTriggerGroupNames(); for (String triggerGroupName : triggerGroupNames) { Set<TriggerKey> triggerKeySet = scheduler .getTriggerKeys(GroupMatcher.triggerGroupEquals(triggerGroupName)); for (TriggerKey triggerKey : triggerKeySet) { Trigger trigger = scheduler.getTrigger(triggerKey); JobKey jobKey = trigger.getJobKey(); JobInfo jobInfo = jobHandler.getJobInfo(jobKey.getGroup(), jobKey.getName()); jobInfos.add(jobInfo); } } return jobInfos; } /** * 添加任务 */ @PostMapping("/add") public JobInfo addJob(@RequestBody JobInfo jobInfo) throws SchedulerException, ClassNotFoundException { jobHandler.addJob(jobInfo); return jobInfo; } /** * 暂停任务 */ @RequestMapping("/pause") public void pauseJob(@RequestParam("jobGroup") String jobGroup, @RequestParam("jobName") String jobName) throws SchedulerException { jobHandler.pauseJob(jobGroup, jobName); } /** * 继续任务 */ @RequestMapping("/continue") public void continueJob(@RequestParam("jobGroup") String jobGroup, @RequestParam("jobName") String jobName) throws SchedulerException { jobHandler.continueJob(jobGroup, jobName); } /** * 删除任务 */ @RequestMapping("/delete") public boolean deleteJob(@RequestParam("jobGroup") String jobGroup, @RequestParam("jobName") String jobName) throws SchedulerException { return jobHandler.deleteJob(jobGroup, jobName); } }
四、开启服务自动执行任务
在JobHandler.java中 增加init方法,并为方法添加@PostConstruct注解,然后在init方法中调用添加任务的方法。
@PostConstruct
是 Java EE(现在更名为 Jakarta EE)规范中定义的一个注解,用于指定在依赖注入完成之后需要执行的方法。具体来说,
@PostConstruct
注解标注的方法将会在对象创建后,但在依赖注入完成之后被调用。这使得开发者可以执行一些在对象初始化之后需要进行的操作。主要作用包括:
初始化操作: 通过
@PostConstruct
注解,你可以在对象创建后执行一些初始化逻辑。这对于那些需要在对象被完全构建之后执行的操作很有用。依赖注入完成后的处理: 通常,
@PostConstruct
方法用于确保依赖注入已经完成,可以在这个方法中执行那些需要依赖注入值的操作。
五、相关问题
1. 单线程与多线程任务调度的区别
单线程运行任务不同任务之间串行,任务A运行时间会响应任务B运行间隔,这是我们不想看到的。
多线程任务调度直接不互相影响,因为使用不同的线程执行任务。
2. 任务调度持久化的好处
如果任务调度没有持久化,而任务又是基于动态设置,不是开机自启的,会有一个问题,服务重启之后设置的任务都会失效了。如果任务整合持久化之后,设置的动态任务信息就会保存到数据库,开机自启就会加载这些数据库信息,就会按照原来的设置运行任务。
注意第二次启动要把自动生成表的配置关掉。
3. Quartz 集群执行与单机执行区别
Quartz是一个开源的作业调度框架,用于在Java应用程序中调度任务。Quartz集群和非集群的区别主要体现在以下几个方面:
-
高可用性:Quartz集群可以提供高可用性,即使其中一个节点出现故障,其他节点仍然可以继续工作。而非集群模式下,如果应用程序所在的服务器出现故障,任务调度将会停止。
-
负载均衡:Quartz集群可以通过将任务分配给不同的节点来实现负载均衡。这意味着任务将在集群的各个节点上分布,从而提高系统整体的性能和吞吐量。非集群模式下,所有的任务将在单个节点上运行,可能会导致性能瓶颈。
-
数据共享:Quartz集群可以共享任务调度的数据,包括作业和触发器等。这意味着当一个节点添加或删除任务时,其他节点也能够感知到。非集群模式下,每个节点都有自己独立的任务调度数据,可能导致数据不一致。
需要注意的是,Quartz集群需要配置和管理多个节点,可能需要更多的系统资源和维护工作。非集群模式则相对简单,适用于小规模的应用程序。选择使用哪种模式应根据具体的需求和系统要求来决定。