Java开发定时任务有三种方案:
1.JDK的Timer,简单易用,无法满足复杂的定时规则,很少用。
2.Spring Task,易用,支持注解和配置文件两种形式,适用于单体项目架构。
3.第三方组件Quartz,功能强大,使用笨重,适用于分布式项目架构。
JDK实现任务调度
public class JdkTaskTest {
//使用main测试,test方法测试有个bug,方法运行直接结束,看不到定时任务的效果。
public static void main(String[] args) {
//创建Timer对象
Timer timer = new Timer();
//创建任务对象
TimerTask timerTask = new TimerTask(){
@Override
public void run() {
System.out.println("定时任务......");
}
};
//执行定时任务timerTask,立刻执行,每两秒执行一次
timer.schedule(timerTask,new Date(),2000);
}
}
Spring-task实现任务调度
1.在IDEA中创建项目时,选择spring项目,并导入spring web依赖,再创建。
2.编写启动类,在启动类上添加 @EnableScheduling 注解,开启任务调度。
3.在main中创建任务类测试
@Component
public class MyTask {
@Scheduled(cron = "*/1 * * * * *") //每秒执行1次
public void task1(){
System.out.println(Thread.currentThread().getName()+":task1--->"+ LocalDateTime.now());
}
}
可以看到运行后每秒执行一次
4.在task1中添加sleep,使其每次执行后睡5秒,会发现两次执行任务相差6秒。执行任务1秒,睡5秒。
5.添加task2,同样设置每秒执行一次,不设置sleep。然而运行的时候发现task2两次执行也差6秒。因为task2受到了task1的影响,Spring-task执行任务是单线程,处理任务能力有限,不适合分布式架构的任务调度。
6.Cron表达式
表达式中有6或7个时间元素,空格分隔,从左到右依次是:秒、分钟、小时、日、月、星期几、年。
*:表示该元素无限制,比如分,表示每分钟。
?:用在日和星期几,因为这两个元素可能会产生冲突,所以可以定义其中一个,另一个用?
-:表示区间,两个数字的连接符。
,:同元素指定多个值,用逗号分隔
/:表示增加幅度。分种0/15表示0,15,30,45分。*/15表示从0开始。
可在网站中自动生成:在线Cron表达式生成器
Quartz 基本应用
官方文档:Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API
1.Quartz可以用来执行定时任务,比Timer增加了很多功能,比如持久性作业,保持调度定时的状态;作业管理,对调度作业进行有效管理。
2.Quartz 的核心类有三部分:任务 Job (实现execute()),触发器 Trigger(SimpleTrigger、CronTrigger),调度器 Scheduler(负责基于Trigger触发器执行任务)。
3.案例
(1)导入第三方组件Quartz的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
(2)在main中新建任务类
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("任务被执行了");
}
}
(3)在test中新建测试类,创建任务调度器、Job任务类、触发器,确定定时任务的执行时机,最后执行任务。
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
//1.创建任务调度器
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
//2.创建Job 任务类
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1","group1")
.build();
//3.创建触发器,定时任务的执行时机
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
//使用简单触发器,每3s执行一次
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever())
//从现在开始执行
.startNow()
.build();
//4.执行任务
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
}
}
(4)运行后可以看到每3秒执行一次
QuartzAPI
JobDetail
作用:绑定 Job,是一个任务实例,有许多扩展参数。
主要字段 | 含义 |
name | 任务名称 |
group | 任务分组,默认分组DEFAULT |
jobClass | 要执行的Job实现类 |
jobDataMap | 任务参数信息,JobDetail、Trigger可以用它设置参数信息 |
JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。
没有JobDetail的话会出现并发访问的情况,数据不安全。
每次执行根据JobDetail创建新的Job示例,避免并发访问。
public class MyJob2 implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
//获得job参数
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
//获得trigger参数
JobDataMap triggerDataMap = context.getTrigger().getJobDataMap();
System.out.println("执行任务,job参数:"+jobDataMap.get("jk1")+","+jobDataMap.get("jk2")+",trigger参数:"+triggerDataMap.get("tk1")+","+triggerDataMap.get("tk2"));
}
}
public class QuartzTest2 {
public static void main(String[] args) throws SchedulerException {
//1.创建任务调度器
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
//2.创建JobDetail对象
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("jk1","jobvalue1");
jobDataMap.put("jk2","jobvalue2");
JobDetail jobDetail = JobBuilder.newJob(MyJob2.class)
.withIdentity("job2","group1")
//存放参数
.setJobData(jobDataMap)
.build();
//3.创建触发器
JobDataMap jobDataMap2 = new JobDataMap();
jobDataMap2.put("tk1","tvalue1");
jobDataMap2.put("tk2","tvalue2");
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
//每3s执行一次
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever())
.usingJobData(jobDataMap2)
.build();
//4.执行触发器
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
}
}
3秒执行一次,两个任务都执行。
SimpleTrigger
是简单触发器,可以实现指定时间段内执行一次任务或循环执行任务,还能设置运行次数。
执行指定次数的任务如下,每3秒执行一次,一共执行3次。次数限制写n,实际运行次数是n+1。
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
//每3s执行一次
.startNow()
//执行次数是n+1
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).withRepeatCount(2))
.usingJobData(jobDataMap2)
.build();
CronTrigger
基于日历的任务调度器,实际应用中较常用。
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group1")
.startNow()
.withSchedule(
//使用日历触发器
CronScheduleBuilder.cronSchedule("0/1 * * * * ? "))
.build();
SpringBoot整合Quartz
实现动态任务调度,作业管理。
案例实现
1.编写pom.xml,添加需要的依赖,有web、quartz、jdbc、mysql、lombok、json处理。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.psjj</groupId>
<artifactId>quartz-study2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>quartz-study2</name>
<description>quartz-study2</description>
<properties>
<java.version>8</java.version>
</properties>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.编写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: Bwu_2021320068
# 必须指定数据源类型
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
这些配置基本够用,如果要配置更多,查看官网
3.配置application.properties 自动生成表
spring.quartz.jdbc.initialize-schema: always
spring.quartz.job-store-type: jdbc
4.实体类
@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;
}
5.任务类MyTask
@DisallowConcurrentExecution:同一个任务必须在上一次执行完毕之后,再按照corn时间执行,不会并行执行。
@PersistJobDataAfterExecution:下一个任务用到上一个任务的修改数据(定时任务里面的jobData数据流转)。
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
@Slf4j
@Component
public class MyTask extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
log.info("TimeEventJob正在执行..." + LocalDateTime.now());
// 执行9秒
try {
Thread.sleep(9000);
log.info("TimeEventJob执行完毕..." + LocalDateTime.now());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
6.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;
}
}
7.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);
}
}
8.测试
(1)选择post方式,http://127.0.0.1:80/job/add,新增任务,任务开始执行。
(2)选择get方式,http://127.0.0.1:80/job/pause?jobGroup=group1&jobName=job1,任务暂停执行。
(3)选择get方式,http://127.0.0.1:80/job/all,查询所有任务,只有上面新增的一个任务。
9.开启服务自动执行任务
在JobHandler中添加init方法和addMyTask方法
@PostConstruct
public void init(){
addMyTask();
}
public void addMyTask() {
try {
JobInfo jobInfo = new JobInfo();
jobInfo.setJobName("job1");
jobInfo.setJobGroup("group1");
jobInfo.setTriggerName("trigger1");
jobInfo.setTriggerGroup("triggerGroup1");
jobInfo.setClassName("top.psjj.task.MyTask");
jobInfo.setCron("0/1 * * * * ? *");
this.addJob(jobInfo);
} catch (SchedulerException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
开启后自动执行任务
单线程与多线程执行任务调度的区别
1.单线程运行任务不同任务之间串行,任务A运行时间会影响任务B运行间隔。
2.多线程中,不同任务各自在不同的线程上,不会互相干扰。
任务1相隔时间长,没影响到任务2。
任务调度持久化的好处
任务基于动态设置,任务调度没有持久化,服务重启后设置的任务会失效。
任务整合持久化,动态任务信息会保存到数据库,开机自启会加载数据库信息,按照原来的设置运行任务。
大概流程是,第一次运行application时添加任务,任务运行。停止application,任务信息自动存入数据库。再次重启application,发现任务仍然在执行。
需要注意的是,在重启application前,需要将application.properties里的内容注释掉,否则每次启动application时都会新建表,将存好数据的表替换成空表,无法实现任务调度持久化。
Quartz 集群执行与单机执行区别
1. 高可用性:Quartz集群提供高可用性,一个节点出现故障,其他节点可以继续工作。非集群模式下,应用程序所在的服务器出现故障,任务调度将会停止。
2. 负载均衡:Quartz集群可以通过将任务分配给不同的节点来实现负载均衡,提高系统整体的性能和吞吐量。非集群模式下,所有的任务将在单个节点上运行,可能会导致性能瓶颈。
3. 数据共享:Quartz集群可以共享任务调度的数据,包括作业和触发器等。非集群模式下,每个节点都有自己独立的任务调度数据,可能导致数据不一致。
Quartz集群需要配置和管理多个节点,需要更多的系统资源和维护工作。
非集群模式相对简单,适用于小规模的应用程序。
总结
简述一下什么是任务调度?
答:任务调度就是按照特定时间规则执行系统某个固定的业务逻辑。任务调度底层是使用jdk的Timer实现的。单体项目建议使用Spring-task任务调度技术,分布式架构建议使用quartz任务调度框架。Spring-task是单线程运行旳,Quartz是多线程运行的,且功能更为丰富,支持作业管理。
说一下你都用过什么任务调度技术,他们的区别是什么?
答:Spring-task是单线程,且功能简单。执行任务只需开启开关@EnableScheduling,在要执行的任务方法上加
@Scheduled(cron = "*/1 * * * * *")注解。它的使用弊端:
1. 任务A的执行时间会影响任务B的执行间隔,但是任务A和任务B是两个任务,不应该相互影响。
2. 没有固定组件,持久化等功能,也就没法形成作业系统
Quartz是多线程的高可用的任务调度框架,支持持久化,多线程,集群模式,且有固定组件结构Job、Trigger、scheduler。他的优点一一说明
1. 有固定组件,有持久化功能,这样就能基于Quartz开发一个任务调度系统,通过UI界面去管理任务调度。
2. 任务进行持久化之后,重启服务器会加载持久化的任务继续执行。
3. 任务支持集群模式,如果任务调度模块是一个集群n个节点,那么任务调度不会因为一个节点挂掉而挂掉,且任务在集群之间形成负载均衡。