定时任务
在 Java 中开发定时任务主要有三种解决方案:
- JDK 自带的 Timer
- Spring Task
- 第三方组件 Quartz 。
特点:
Timer 是 JDK 自带的定时任务工具,其简单易用,但是对于复杂的定时规则无法满足,在实际项目开发中也很少使用到。而 Spring Task使用起来很简单,除 Spring 相关的包外不需要额外的包,而且支持注解和配置文件两种形式。 Quartz 功能强大,但是使用起来相对笨重。
建议:
-
单体项目架构使用Spring Task
-
分布式项目架构使用Quartz
JDK实现任务调度
简单例子:
/**
* 基于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.导入spring-boot-starter-web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.在启动类上添加@EnableScheduling注解
3.编写测试任务类
@Component
public class SpringTask {
@Scheduled(cron = "*/1 * * * * *")
public void task1() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+":task1--->"+ LocalDateTime.now());
}
}
Cron表达式
Spring Task依靠Cron表达式配置定时规则。Cron表达式是一个字符串,分为6或7个域,每一个域代表一个含义,以空格隔开。有如下两种语法格式:
- Seconds Minutes Hours DayofMonth Month DayofWeek Year
- Seconds Minutes Hours DayofMonth Month DayofWeek
Seconds(秒):域中可出现 , - * / 四个字符,以及0-59的整数
- * :表示匹配该域的任意值,在Seconds域使用 * ,表示每秒钟都会触发
- , :表示列出枚举值。在Seconds域使用 5,20 ,表示在5秒和20秒各触发一次。
- - :表示范围。在Seconds域使用 5-20 ,表示从5秒到20秒每秒触发一次
- / :表示起始时间开始触发,然后每隔固定时间触发一次。在Seconds域使用 5/20 , 表示5秒触发一次,25秒,45秒分别触发一次。
Minutes(分):域中可出现 , - * / 四个字符,以及0-59的整数
Hours(时):域中可出现 , - * / 四个字符,以及0-23的整数
DayofMonth(日期):域中可出现 , - * / ? L W C 八个字符,以及1-31的整数
C :表示和当前日期相关联。在DayofMonth域使用 5C ,表示在5日后的那一天触发,且每月的那天都会触发。比如当前是10号,那么每月15号都会触发。
L :表示最后,在DayofMonth域使用 L ,表示每个月的最后一天触发。
W :表示工作日,在DayofMonth域用 15W ,表示最接近这个月第15天的工作日触发,如果15号是周六,则在14号即周五触发;如果15号是周日,则在16号即周一触发;如果15号是周二则在当天触发。
Month(月份):域中可出现 , - * / 四个字符,以及1-12的整数或JAN-DEC的单词缩写
DayofWeek(星期):可出现 , - * / ? L # C 八个字符,以及1-7的整数或SUN-SAT 单词缩写,1代表星期天,7代表星期六
C :在DayofWeek域使用 2C ,表示在2日后的那一天触发,且每周的那天都会触发。比如当前是周一,那么每周三都会触发。
L :在DayofWeek域使用 L ,表示在一周的最后一天即星期六触发。在DayofWeek域使用 5L ,表示在一个月的最后一个星期四触发。
# :用来指定具体的周数, # 前面代表星期几, # 后面代表一个月的第几周,比如 5#3 表示一个月第三周的星期四。
? :在无法确定是具体哪一天时使用,用于DayofMonth和DayofWeek域。例如在每月的20日零点触发1次,此时无法确定20日是星期几,写法如下: 0 0 0 20 * ? ;或者在每月的最后一个周日触发,此时无法确定该日期是几号,写法如下: 0 0 0 ? * 1L
Year(年份):域中可出现 , - * / 四个字符,以及1970~2099的整数。该域可以省略,表示每年都触发。
Quartz
Quartz 的核心类有以下三部分:
-
任务 Job : 需要实现org.quartz.Job 接口的任务类,实现
execute()
方法,执行后完成任务。 -
触发器 Trigger : 实现触发任务去执行的触发器,触发器 Trigger 最基本的功能是指定 Job 的执行时间,执行间隔,运行次数等。
-
调度器 Scheduler : 任务调度器,负责基于
Trigger
触发器,来执行 Job任务。
JobDetail
可以只创建一个 job 类,然后创建多个与该 job 关联的 JobDetail 实例,每一个实例都有自己的属性集和 JobDataMap,最后,将所有的实例都加到 scheduler 中。
//1.创建任务调度器
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
//2.创建JobDetail实例
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("param1","value1");
jobDataMap.put("param2","value2");
JobDetail jobDetail = JobBuilder.newJob(MyJob2.class)
.withIdentity("job2","group1")
.setJobData(jobDataMap).build();
当一个 trigger 被触发时,与之关联的 JobDetail 实例会被加载,JobDetail 引用的 job 类通过配置在 Scheduler 上的 JobFactory 进行初始化。
默认的 JobFactory 实现,仅仅是调用 job 类的 newInstance() 方法,然后尝试调用 JobDataMap 中的 key 的 setter 方法
也可以自定义 JobFactory 实现,比如让 IOC 或 DI 容器可以创建/初始化 job 实例
SimpleTrigger
SimpleTrigger是接口Trigger的一个具体实现,它可以触发一个已经安排进调度程序(任务执行计划)的任务,并可以指定时间间隔重复执行该任务。
SimpleTrigger 包含几个特点:开始时间、结束时间、重复次数以及重复执行的时间间隔。
重复的次数可以是零,一个正整数,或常量。重复执行的时间间隔可以是零,或者long类型的数值表示毫秒。值得注意的是,零重复间隔会造成触发器同时发生(或接近同时作为调度管理)。
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group1")
.startNow()
.withSchedule(
//使用简单触发器
SimpleScheduleBuilder.simpleSchedule().
//3s间隔执行
withIntervalInSeconds(3).
//执行6次 count+1
withRepeatCount(5))
.build();
CronTrigger
CronTrigger 是基于日历的任务调度器,可以通过表达式来设置时间 。
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2","group1")
.startNow()
.withSchedule(
//使用日历触发器
CronScheduleBuilder.cronSchedule("0/1 * * * * ? "))
.build();
单线程与多线程执行任务调度的区别
单线程与多线程执行任务调度的区别在于任务的执行方式和效率。
在单线程执行任务调度时,任务按照顺序依次执行,每个任务执行完毕后才会执行下一个任务。这种方式适用于任务之间没有依赖关系或者任务之间的执行顺序不重要的情况。单线程执行任务调度的优点是简单、易于实现,但是由于任务是串行执行的,所以执行效率较低,特别是当任务的执行时间较长时,会导致整个调度过程变慢。
而在多线程执行任务调度时,每个任务都可以在独立的线程中并行执行,不受其他任务的影响。这种方式适用于任务之间存在依赖关系或者任务之间的执行顺序很重要的情况。多线程执行任务调度的优点是可以提高任务的执行效率,特别是当任务的执行时间较长时,可以充分利用多核处理器的性能。但是多线程执行任务调度也存在一些问题,比如线程间的同步和资源竞争等,需要额外的处理来保证任务的正确执行。
任务调度持久化的好处
-
高可靠性:持久化存储可以防止任务调度信息的丢失。即使系统发生故障或重启,任务调度信息仍然可以被恢复,确保任务能够按计划执行。
-
持续性:持久化存储可以保证任务调度的持续性。即使系统关闭或任务调度器停止运行,任务调度信息也能够被保存下来。当系统重新启动或任务调度器重新运行时,可以从持久化存储中加载任务调度信息,继续执行之前未完成的任务。
-
灵活性:通过持久化存储,可以对任务调度信息进行灵活的管理和配置。可以随时添加、修改或删除任务调度信息,以满足不同的业务需求。
-
监控和统计:持久化存储可以记录任务调度的执行情况和状态,方便进行监控和统计分析。可以根据任务调度信息的历史记录,进行性能分析、故障排查和优化调整。
-
分布式支持:对于分布式系统或集群环境下的任务调度,持久化存储可以提供共享的任务调度信息,确保各个节点之间的任务调度一致性和协调性。
Quartz 集群执行与单机执行区别
-
高可用性:Quartz集群可以提供高可用性,即使其中一个节点出现故障,其他节点仍然可以继续工作。而非集群模式下,如果应用程序所在的服务器出现故障,任务调度将会停止。
-
负载均衡:Quartz集群可以通过将任务分配给不同的节点来实现负载均衡。这意味着任务将在集群的各个节点上分布,从而提高系统整体的性能和吞吐量。非集群模式下,所有的任务将在单个节点上运行,可能会导致性能瓶颈。
-
数据共享:Quartz集群可以共享任务调度的数据,包括作业和触发器等。这意味着当一个节点添加或删除任务时,其他节点也能够感知到。非集群模式下,每个节点都有自己独立的任务调度数据,可能导致数据不一致。
面试题
【1】简述一下什么是任务调度?
答:任务调度就是按照特定时间规则执行系统某个固定的业务逻辑。任务调度底层是使用jdk的Timer实现的。单体项目建议使用Spring-task任务调度技术,分布式架构建议使用quartz任务调度框架。Spring-task是单线程运行旳,Quartz是多线程运行的,且功能更为丰富,支持作业管理。
【2】说一下你都用过什么任务调度技术,他们的区别是什么?
答:Spring-task是单线程,且功能简单。执行任务只需开启开关@EnableScheduling,在要执行的任务方法上加
@Scheduled(cron = "*/1 * * * * *")注解。它的使用弊端:
-
任务A的执行时间会影响任务B的执行间隔,但是任务A和任务B是两个任务,不应该相互影响。
-
没有固定组件,持久化等功能,也就没法形成作业系统
Quartz是多线程的高可用的任务调度框架,支持持久化,多线程,集群模式,且有固定组件结构Job、Trigger、scheduler。他的优点一一说明
-
有固定组件,有持久化功能,这样就能基于Quartz开发一个任务调度系统,通过UI界面去管理任务调度。
-
任务进行持久化之后,重启服务器会加载持久化的任务继续执行。
-
任务支持集群模式,如果任务调度模块是一个集群n个节点,那么任务调度不会因为一个节点挂掉而挂掉,且任务在集群之间形成负载均衡。