定义任务概述
解释
通过时间表达式这一方式来进行任务调度的被称为定时任务
分类
单机定时任务
1、单机的容易实现,但应用于集群环境做分布式部署,就会带来重复执行
2、解决方案有很多比如加锁、数据库等,但是增加了很多非业务逻辑
分布式定时任务
1、把需要处理的计划任务放入到统一的平台,实现集群管理调度与分布式部署的定时任务 叫做分布式定时任务
2、支持集群部署、高可用、并行调度、分片处理等
常见单机定时任务
Java自带的java.util.Timer类
package com.timer;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author: li.wang
* @Des:
* @Date: 2022-07-25 14:10
*/
public class MyDemoTimerTask {
public static void main(String[] args) {
// 定义一个任务
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("the task is running at:" + new Date());
}
};
// 计时器
Timer timer = new Timer();
/** 添加执行任务(延迟 1s 执行,每 2s 执行一次) */
timer.schedule(timerTask, 1000, 2000);
/** new Date()时间之后,每秒执行一次 -- 时间可自行指定 */
timer.schedule(timerTask, new Date(), 1000);
/** 当前时间之后,每秒执行一次 -- 默认当前时间之后 */
timer.schedule(timerTask,1000);
/** new Date()时间执行一次 -- new Date()可换成其他指定时间且 只会执行一次 */
timer.schedule(timerTask,new Date());
/** 取消timer指定的调度 */
timer.cancel();
}
}
PS:
Timer 是 JDK 自带的定时任务执行类
多任务时,存在任务延迟执行的bug,不太推荐使用
基于线程池的ScheduledExecutorService
package com.schedule;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @Author: li.wang
* @Des:
* @Date: 2022-07-25 14:29
*/
public class DemoScheduledExecutorService {
public static void main(String[] args) {
/** 创建任务调度线程池 */
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
DemoRunTask demoRunTask = new DemoRunTask();
/** 分配任务 -- 2s 后开始执行,每 3s 执行一次 ,与 scheduleWithFixedDelay 方法参数与功能一致
* demoRunTask 实际执行任务的类
* var2 延迟多久后执行,例如本例 2
* var4 执行周期 ,例如本例 3
* TimeUnit var2与var4使用单位
* */
scheduledExecutorService.scheduleAtFixedRate(demoRunTask, 2, 3, TimeUnit.SECONDS);
/** 分配任务 -- 2s 后开始执行,只执行一次
* demoRunTask 实际执行任务的类
* var2 延迟多久后执行,例如本例 2
* TimeUnit var2
* */
scheduledExecutorService.schedule(demoRunTask,2,TimeUnit.SECONDS);
/** 分配任务 -- 2s 后开始执行,每 3s 执行一次
* demoRunTask 实际执行任务的类
* var2 延迟多久后执行,例如本例 2
* var4 执行周期 ,例如本例 3
* TimeUnit var2与var4使用单位
* */
scheduledExecutorService.scheduleWithFixedDelay(demoRunTask, 2, 3, TimeUnit.SECONDS);
}
}
PS:
ScheduledExecutorService 也是 JDK 1.5 自带的 API
较为容易上手,且各任务间无论是 异常 还是 执行时间 均不会相互影响
SpringBoot使用注解方式开启定时任务
启动类添加注解 - @EnableScheduling
package com.sctech.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author li.wang
*/
@SpringBootApplication
@EnableScheduling
public class IotApplication {
public static void main(String[] args) {
SpringApplication.run(IotApplication.class, args);
}
}
任务类 - 注入容器并在方法添加@Scheduled注解
package com.sctech.demo.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduleTask {
@Scheduled(fixedRate = 1000 * 60)
public void fixeOneMinuteRate() {
System.out.println("任务已被成功调用...");
}
@Scheduled(cron = "0 0/30 * * * ?")
public void fix30MinuteRate() {
System.out.println("任务已被成功调用...");
}
}
PS:
Spring Framework 自带的定时任务,亦称为 Spring Task
Spring Boot 启动后会自动加载并执行定时任务
Quartz(支持分布式)
作为分布式时总结:
基于db可实现高可用,但缺少了分布式的并行调度功能,不支持任务分片、没UI界面管理,并行调度、失败策略缺少
演示从数据库获取时间点列表,生成quartz调度池,并可对调度池增删改查
接口
package com.demo.control.service;
import com.demo.control.domain.TimeMst;
import java.util.List;
/**
* @Author: li.wang
* @Des:
* @Date: 2022-07-23 15:16
*/
public interface IQuartzService {
/**
* 生成调度池
*
* @param list 时间调度集合
* @return 调度任务集合
*/
public boolean buildSchedulePool(List<TimeMst> list);
/**
* 向调度池增加新的调度
*
* @param time 时间调度集合
* @return 调度任务集合
*/
public boolean buildScheduleOne(TimeMst time) ;
/**
* 关闭调度池
*
* @return 调度任务对象信息
*/
public boolean unSchedulePool();
/**
* 从调度池移除调度
*
* @param id 时间id
* @return 调度任务对象信息
*/
public boolean unScheduleOne(Long id);
/**
* 从调度池移除多个调度
*
* @param ids 时间id集合
* @return 调度任务对象信息
*/
public boolean unScheduleMulti(List<Long> ids);
/**
* 从调度池更新一个调度
*
* @param timeMst 时间
* @return 调度任务对象信息
*/
public boolean updateScheduleOne(TimeMst timeMst);
/**
* 销毁调度线程
*
* @return 结果
*/
public int shutdownSchedulePool();
}
接口实现
package com.demo.control.service.impl;
import com.google.common.collect.Sets;
import com.demo.common.utils.DateUtils;
import com.demo.control.domain.TimeMst;
import com.demo.control.job.InitScheduleConstant;
import com.demo.control.job.ScheduleJob;
import com.demo.control.service.IQuartzService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 调度池服务
*
* @author li.wang
* @date 2022-07-23
*
*/
@Service
@Slf4j
public class QuartzServiceImpl implements IQuartzService
{
/** 获取默认调度器 */
private final Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
public QuartzServiceImpl() throws SchedulerException {
}
@lombok.SneakyThrows
@Override
public boolean buildSchedulePool(List<TimeMst> list) {
HashSet<Trigger> triggerSet = Sets.newHashSet();
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class)
.withIdentity("JobDetailName")
.build();
list.forEach(time -> {
Trigger trigger = TriggerBuilder.newTrigger()
.usingJobData("id",time.getId())
.usingJobData("time",time.getTime())
.withIdentity("TriggerName"+ time.getId(),"group")
.startAt( DateUtils.parseDate(time.getTime()) )
//间隔一天
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24))
.build();
triggerSet.add(trigger);
InitScheduleConstant.Id2TriggerKeyMap.put(time.getId(),trigger.getKey());
});
// true代表jobDetail与Trigger的Key重复则直接替换,不会抛出异常
scheduler.scheduleJob(jobDetail,triggerSet,true);
// 启动调度器
scheduler.start();
return true;
}
@lombok.SneakyThrows
@Override
public boolean buildScheduleOne(TimeMst time) {
if(Objects.isNull(time) || StringUtils.isEmpty(time.getTime())) return false;
//时间格式调整为当天时间
String date = DateUtils.getDate();
time.setTime(date + " " + time.getTime() +":00");
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class)
.withIdentity("JobDetailName"+time.getId())
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.usingJobData("id",time.getId())
.usingJobData("time",time.getTime())
.withIdentity("TriggerName"+ time.getId(),"group")
//指定时间开始
.startAt( DateUtils.parseDate(time.getTime()) )
//从当前时间开始
//.startNow()
//到指定时间调度结束
//.endAt(DateUtils.parseDate( "2023-07-23 15:32:12" ))
//间隔一天
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24))
//间隔30秒
//.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(30))
.build();
scheduler.scheduleJob(jobDetail, trigger);
//存储调度池的触发器Key
InitScheduleConstant.Id2TriggerKeyMap.put(time.getId(),trigger.getKey());
// 启动调度器
scheduler.start();
return true;
}
@SneakyThrows
@Override
public boolean unSchedulePool() {
if( scheduler.isStarted() && InitScheduleConstant.Id2TriggerKeyMap.keySet().size() > 0 ){
List<TriggerKey> triggerKeys = new ArrayList<>(InitScheduleConstant.Id2TriggerKeyMap.values());
scheduler.unscheduleJobs( triggerKeys );
InitScheduleConstant.Id2TriggerKeyMap.clear();
}
return true;
}
@SneakyThrows
@Override
public boolean unScheduleOne(Long id) {
if( scheduler.isStarted() && InitScheduleConstant.Id2TriggerKeyMap.containsKey(id) ){
TriggerKey triggerKey = InitScheduleConstant.Id2TriggerKeyMap.get(id);
scheduler.unscheduleJob( triggerKey );
InitScheduleConstant.Id2TriggerKeyMap.remove(id);
}
return true;
}
@SneakyThrows
@Override
public boolean unScheduleMulti(List<Long> ids) {
ids.forEach(this::unScheduleOne);
return true;
}
@Override
public boolean updateScheduleOne(TimeMst timeMst) {
//移除一条旧调度
unScheduleOne(timeMst.getId());
//新增一条新调度
buildScheduleOne(timeMst);
return true;
}
@SneakyThrows
@Override
public int shutdownSchedulePool() {
if(scheduler.isShutdown()){
return 0;
}
scheduler.shutdown(true);
return 1;
}
}
使用的Bean-TimeMst
package com.zxkw.control.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class TimeMst implements Serializable
{
private static final long serialVersionUID = 1L;
/** 主键 */
private Long id;
/** 分次时间 */
private String time;
}
TimeMst在mysql存储展示
数据截图
实际使用的任务
package com.demo.control.job;
import com.demo.common.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Author: wangli
* @Des:
* @Date: 2022-07-23 15:23
*/
@Slf4j
public class ScheduleJob implements Job {
private SchedulingTask schedulingTask;
public ScheduleJob() {
log.info("ScheduleJob任务被创建");
schedulingTask = SpringUtils.getBean(SchedulingTask.class);
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("任务类:{}被创建执行,其任务名称为:{},触发器名称为:{},触发器本次生效时间为:{},触发器下次生效时间为:{}",
this.getClass().getName(),jobExecutionContext.getJobDetail().getKey().getName(),
jobExecutionContext.getTrigger().getKey().getName(),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),
jobExecutionContext.getTrigger().getNextFireTime());
//执行实际任务
String id = String.valueOf( jobExecutionContext.getTrigger().getJobDataMap().get("id") );
schedulingTask.doJob(id);
log.info("任务执行完毕,触发器名称为:{}",jobExecutionContext.getTrigger().getKey().getName());
}
}
ScheduleJob使用的类
package com.demo.control.job;
import cn.hutool.core.date.DateUtil;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class SchedulingTask {
/**
* 执行任务
* @param id 时间表主键
*/
public void doJob(String id) {
log.info("任务已被调用执行,使用的参数有:{}",id);
}
}
字符串常量展示 - InitScheduleConstant
package com.demo.control.job;
import com.google.common.collect.Maps;
import org.quartz.TriggerKey;
import java.util.Map;
/**
* @Author: li.wang
* @Des:
* @Date: 2022-07-23 18:03
*/
public class InitScheduleConstant {
/** 存储本次所需的TriggerKey - id -> TriggerKey */
public static Map<Long, TriggerKey> Id2TriggerKeyMap = Maps.newConcurrentMap();
}
程序启动,初始化调度池
package com.demo.init;
import com.demo.common.utils.DateUtils;
import com.demo.common.utils.StringUtils;
import com.demo.control.domain.TimeMst;
import com.demo.control.service.IQuartzService;
import com.demo.control.service.ITimeMstService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: li.wang
* @Des:
* @Date: 2022-07-23 14:13
*/
@Component
@Slf4j
public class ScheduleJobRunnerImpl implements ApplicationRunner {
@Autowired
private ITimeMstService timeMstService;
@Autowired
private IQuartzService quartzService;
/**
* 项目初始化运行
* @param args
* @throws Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
//项目加载完bean与service后,开始从数据库中捞取已定义的调度信息
List<TimeMst> timeMst = timeMstService.selectTimeMstList(new TimeMst());
String date = DateUtils.getDate();
//时间格式调整为当天时间
List<TimeMst> list = timeMst.stream().filter(t -> StringUtils.isNotEmpty(t.getTime()))
.peek(t -> t.setTime(date + " " + t.getTime() +":00")).collect(Collectors.toList());
//生成调度池
quartzService.buildSchedulePool(list);
log.info("调度时间表数据{}", StringUtils.isEmpty(list) ? "为空,无需生成调度池" : "已生成调度池");
}
/**
* 项目关闭时运行,此处主要是为了让任务执行完毕再关闭程序
*/
@PreDestroy
public void destroy() {
log.info("程序正在销毁中,开始销毁调度池...");
quartzService.shutdownSchedulePool();
}
}
PS:
quartzService.buildSchedulePool(list)方法接受参数可改为接受形如 "2022-07-23 15:32:12"的字符
串列表,方法体小调一下即可,无需从数据库捞取数据,此处不展示捞取过程