一、JDK实现任务调度
使用JDK自带的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);
}
}
二、Spring-task实现任务调度
1.案例演示
(1)创建项目
(2)在启动器类上添加@EnableScheduling注解,打开任务调度
@SpringBootApplication
@EnableScheduling
public class TaskStudyApplication {
public static void main(String[] args) {
SpringApplication.run(TaskStudyApplication.class, args);
}
}
(3)使用@Scheduled注解,编写任务类测试
@Component
public class SpringTask {
@Scheduled(cron = "*/1 * * * * *")
public void task1() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+":task1--->"+ LocalDateTime.now());
}
}
2.Spring-Task 分析
编写任务调度类,加上5秒的执行时间
@Scheduled(cron = "*/1 * * * * *")
public void task1() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+":task1--->"+ LocalDateTime.now());
Thread.sleep(5000);
}
可以发现任务调度并不是每秒执行1次,而是6秒执行一次。而且执行任务的线程名字都是一样的,由此得出两个结论:
Spring-task 执行任务按照单线程执行并合理执行,不会因为第一个执行任务时间过长而执行第二个。
Spring-task是单线程的处理任务能力有限,不建议处理分布式架构的任务调度。
3.Cron表达式
Cron表达式是一个字符串, 用作@Scheduled注解的参数,来设置定时规则, 由七部分组成, 每部分中间用空格隔开, 每部分的含义如下表所示:
组成部分 | 含义 | 取值范围 |
第一部分 | Seconds (秒) | 0-59 |
第二部分 | Minutes(分) | 0-59 |
第三部分 | Hours(时) | 0-23 |
第四部分 | Day-of-Month(天) | 1-31 |
第五部分 | Month(月) | 0-11或JAN-DEC |
第六部分 | Day-of-Week(星期) | 1-7(1表示星期日)或SUN-SAT |
第七部分 | Year(年) 可选 | 1970-2099 |
另外, cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:
符号 | 含义 |
? | 表示不确定的值。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?” |
* | 代表所有可能的值 |
, | 设置多个值,例如”26,29,33”表示在26分,29分和33分各自运行一次任务 |
- | 设置取值范围,例如”5-20”,表示从5分到20分钟每分钟运行一次任务 |
/ | 设置频率或间隔,如"1/15"表示从1分开始,每隔15分钟运行一次任务 |
L | 用于每月,或每周,表示每月的最后一天,或每个月的最后星期几,例如"6L"表示"每月的最后一个星期五"(springtask框架不支持) |
W | 表示离给定日期最近的工作日,例如"15W"放在每月(day-of-month)上表示"离本月15日最近的工作日"(springtask框架不支持) |
# | 表示该月第几个周X。例如”6#3”表示该月第3个周五(springtask框架不支持) |
三、Quartz组件实现任务调度
1.Quartz介绍
Quartz 是一个功能丰富的开源任务调度框架(job scheduling library)。从最小的独立的 Java 应用程序到最大的电子商务系统,它几乎都可以集成。Quartz 可用于创建简单或复杂的调度,以执行数十、数百个甚至数万个任务;这些任务被定义为标准 Java 组件,这些组件可以执行你想让他做的任何事情。Quartz 调度程序包括许多企业级特性,例如支持 JTA 事务(Java Transaction API,简写 JTA)和集群。
2.Quartz API
Quartz 的核心类有以下三部分:
任务 Job : 需要实现的任务类,实现 execute()
方法,执行后完成任务。
触发器 Trigger : 包括 SimpleTrigger
和 CronTrigger
。
调度器 Scheduler : 任务调度器,负责基于 Trigger
触发器,来执行 Job任务。
(1)JobDetail
JobDetail 的作用是绑定 Job,是一个任务实例,它为 Job 添加了许多扩展参数。
主要字段 | 含义 |
name | 任务名称 |
group | 任务分组,默认分组DEFAULT |
jobClass | 要执行的Job实现类 |
jobDataMap | 任务参数信息,JobDetail、Trigger都可以使用JobDataMap来设置一些参数或者信息 |
每次Scheduler
调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()
的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。
注意:为什么设计成JobDetail + Job,不直接使用Job?
JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。
这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail+Job
方式,Sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以 规避并发访问 的问题。
(2)SimpleTrigger
这是比较简单的一类触发器,用它能实现很多基础的应用。使用它的主要场景包括:
-
在指定时间段内,执行一次任务
最基础的 Trigger 不设置循环,设置开始时间。
-
在指定时间段内,循环执行任务
在 1 基础上加上循环间隔。可以指定 永远循环、运行指定次数
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group1")
.startNow()
.withSchedule(
//使用简单触发器
SimpleScheduleBuilder.simpleSchedule().
//3s间隔执行
withIntervalInSeconds(3).
//始终执行
repeatForever())
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group1")
.startNow()
.withSchedule(
//使用简单触发器
SimpleScheduleBuilder.simpleSchedule().
//3s间隔执行
withIntervalInSeconds(3).
//执行6次 count+1
withRepeatCount(5))
.build();
(3) CronTrigger
cronTrigger是基于日历的任务调度器,可以通过表达式来设置时间
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group1")
.startNow()
.withSchedule(
//使用日历触发器
CronScheduleBuilder.cronSchedule("0/1 * * * * ? "))
.build();
3.SpringBoot整合Quartz
(1)创建项目,导入依赖
(2)编写application.yml配置文件,添加qurtz相关配置,编写application.properties配置文件,自动生成表
(3)编写实体类
(4)编写任务类
@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);
}
}
}
这个类就继承了QuartzJobBean,也可以实现Job接口,这个类就是任务需要具体执行的业务操作类,类上面添加了两个注解,这两个注解的目的就是让同一个任务必须在上一个任务执行完毕之后再按照触发后续执行,以及定时任务里面的JobDataMap,能够在任务中流转以及修改更新;不添加注解的情况下,JobDataMap里面的数据不能在任务之间流转,以及任务的触发不会参照上一任务是否执行完毕。
(5)编写JobHandle,编写任务的开关停删操作接口
@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;
}
}
(6)编写Controller,调用JobHandle中的接口实现任务操作
@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);
}
}
四、常见问题
1.单线程与多线程执行任务调度的区别
单线程运行任务不同任务之间串行,任务A运行时间会响应任务B运行间隔
2.任务调度持久化的好处
如果任务调度没有持久化,而任务又是基于动态设置,不是开机自启的,会有一个问题,服务重启之后设置的任务都会失效了。如果任务整合持久化之后,设置的动态任务信息就会保存到数据库,开机自启就会加载这些数据库信息,就会按照原来的设置运行任务。
3.Quartz 集群执行与单机执行区别
Quartz是一个开源的作业调度框架,用于在Java应用程序中调度任务。Quartz集群和非集群的区别主要体现在以下几个方面:
-
高可用性:Quartz集群可以提供高可用性,即使其中一个节点出现故障,其他节点仍然可以继续工作。而非集群模式下,如果应用程序所在的服务器出现故障,任务调度将会停止。
-
负载均衡:Quartz集群可以通过将任务分配给不同的节点来实现负载均衡。这意味着任务将在集群的各个节点上分布,从而提高系统整体的性能和吞吐量。非集群模式下,所有的任务将在单个节点上运行,可能会导致性能瓶颈。
-
数据共享:Quartz集群可以共享任务调度的数据,包括作业和触发器等。这意味着当一个节点添加或删除任务时,其他节点也能够感知到。非集群模式下,每个节点都有自己独立的任务调度数据,可能导致数据不一致。
需要注意的是,Quartz集群需要配置和管理多个节点,可能需要更多的系统资源和维护工作。非集群模式则相对简单,适用于小规模的应用程序。选择使用哪种模式应根据具体的需求和系统要求来决定。