1.@Scheduled注解介绍
在spring boot的项目中需要使用到定时任务的时候,可以使用@Scheduled注解,这只是在一个JVM进程中很适用,如果涉及到服务器是集群的情况下,建议使用任务调度平台。这样任务调度平台会在多台服务器中选择一台进行定时任务的执行。该注解位于spring-context.jar
包中
2.@Scheduled相关属性说明
属性 | 说明 |
cron():String | 使用Cron表达式创建定时任务,值可以是字符串也“0 * * * * MON-FRI” 可以是${...} 获取配置文件中定义的表达式 |
zone():String | 指定cron的时区,默认是空字符串,表示本地时区 |
fixedDelay():long | 任务执行的时间间隔,表示第一次任务执行完毕和第二次任务开始之间的时间,单位:毫秒 |
fixedRate():long | 每隔多久执行一次任务,第一次任务开始和第二次任务开始之间的时间,单位:毫秒 |
initialDelay():long | 表示第一次执行fixedDelay 和fixedRate 要等待的时间,单位:毫秒 |
3.@Scheduled简单使用
(1)首先要知道@Scheduled注解要生效需要在系统启动类或配置类上添加@EnableScheduling注解,这里在系统的启动类上加@EnableScheduling注解。如下所示:
(2)创建一个定时任务类TestSchedule,使用@Component注解标注,交给容器管理。定义一个方法taskScheduledOne,并在方法上加上注解@Scheduled以及cron表达式,如下图所示@Scheduled(cron = "0/10 * * * * ?")表示该定时任务每10秒钟执行一次。
(3)执行效果如下:
如上图的打印效果所示, 到这里,一个简单的定时任务就已经完成了
4.可能出现的问题一:两个定时任务执行时间间隔问题
测试代码如下所示:
@Component
public class TestSchedule {
/**
* 每十秒钟执行一次
*/
@Scheduled(cron = "0/10 * * * * ?")
public void taskScheduledOne() throws InterruptedException {
Thread t = Thread.currentThread();
System.out.println("taskScheduledOne "+ DateTimeUtils.dateToString(new Date()) +" ThreadID:"+ t.getId() +" "+t.getName());
Thread.sleep(5000);
System.out.println("taskScheduledOne End " + DateTimeUtils.dateToString(new Date()) + " ThreadID:" + t.getId() + " " + t.getName());
}
/**
* 每三秒钟执行一次
*/
@Scheduled(cron = "0/3 * * * * ?")
public void taskScheduledTwo() {
Thread t = Thread.currentThread();
System.out.println("taskScheduledTwo " + DateTimeUtils.dateToString(new Date()) + " ThreadID:" + t.getId() + " " + t.getName());
}
}
运行代码,最初的部分结果打印如图所示:我们发现定时任务2和定时任务1都是一个线程(线程id为48)执行的,定时任务2是每3秒钟执行一次,但这里出现了问题,如下图中的箭头所示,定时任务2在“ 2023/06/08 20:24:39 ”执行完,下一次的执行完的时间按理说应该是“ 2023/06/08 20:24:42 ”,但结果显示时间是2023/06/08 20:24:45,说明这里出现了问题。
问题解释:因为Spring中@EnableScheduling和@Scheduled标注的定时任务默认是单线程执行的,所以定时任务2 在箭头A 执行完,定时任务1开始执行,定时任务1 和 定时任务2 都是同一个线程执行的,这里的定时任务1执行花费5s左右的时间,已经超过了定时任务2的3s执行周期,导致定时任务2被阻塞了。所以定时任务2再执行打印时,就出现在箭头B这个地方了,时间差已经超过了3s。
5.可能出现的问题二:使用@Async和@EnableAsync异步执行任务
Spring的定时任务包中提供了@EnableAsync和@Async注解用于多线程异步执行任务。
在启动类上添加@EnableAsync注解,并在TestSchedule类上标注@Async注解,表示该类中所有标注了@Scheduled的方法都使用异步处理方式。
再次执行之前的代码看一下效果:
定时任务2时间间隔均是3s,定时任务1时间间隔也是10s,且非单线程操作了。
这里看似解决了上述问题一,但其实可能会引入第二个问题,就是定时任务的执行时间过长,超过定时任务执行的周期。修改一下上述的代码,将定时任务1线程沉睡的时间由5s改为11s。再次执行代码:
Thread.sleep(5000);
// 改为
Thread.sleep(11000);
执行效果如下:定时任务2正常执行。但定时任务1执行出现了线程交叉执行。
原因解释:定时任务1的执行时间超过了定时任务的执行周期,在某个线程执行定时任务1时,还没结束,下一个线程又开始执行定时任务1了,这就是使用@EnableAsync和@Async异步执行定时任务可能会出现的问题。
6.问题一和问题二的解决方案
(1)去掉如下注释的注解
(2)创建一个任务配置类ScheduleConfig 实现SchedulingConfigurer接口的configureTasks方法,使用参数taskRegistrar为任务调度创建线程池;
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod = "shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(10);
}
}
运行结果如下图所示:定时任务2执行正常,定时任务1执行也正常,不会出现交叉现象,定时任务1第二次执行的时间会等第一次执行完毕之后下一个任务任务调度时间点开始才会执行。
7.总结
SpringBoot中可以使用@EnableScheduling和@Scheduled注解实现定时任务调度,但是注意默认所有任务都被单个线程调度的,有可能任务之间发生阻塞现象,可以使用@EnableAsync和@Async注解实现异步多线程任务调度,但需要注意任务执行时间如果大于任务调度周期时间,可能出现同一个任务交叉执行的情况。上述6可以解决相关问题。