目录
1. 前言
2. 创建SpringBoot项目
3. Maven依赖引入
4. 创建定时任务
5. 问题:执行时间延迟和单线程执行
5.1 问题原因
5.2 解决方式
1. 前言
在现代化应用中,定时任务(Scheduled Tasks)是不可或缺的一部分,它们允许开发者在预定的时间间隔内自动执行特定任务,如数据同步、报告生成、系统维护等。Spring Boot作为一款流行的Java开发框架,提供了简洁而强大的定时任务实现方式。
Spring Boot通过内置的@Scheduled注解和@EnableScheduling配置,使得开发者能够轻松地在应用中集成定时任务功能。无需引入额外的依赖或复杂的配置,只需在方法上添加@Scheduled注解,并指定任务的执行计划,Spring Boot便会自动处理任务的调度和执行。
本教程将带您逐步了解Spring Boot定时任务的实现过程,包括基本的配置和注解使用、常见的执行计划设置、以及高级功能如动态定时任务、多线程定时任务等。通过本教程的学习,您将能够掌握在Spring Boot中高效实现定时任务的方法和技巧,为您的应用增添更多自动化和智能化的功能。
请注意,随着技术的不断发展,Spring Boot的版本和特性也在不断更新。因此,建议参考最新的Spring Boot官方文档和相关教程来获取最准确的信息。同时,本教程也提供了实用的示例和代码,帮助您更好地理解和应用定时任务功能。
2. 创建SpringBoot项目
首先,我们需要创建SpringBoot项目。在创建SpringBoot项目时,可以选择使用Spring Initializr来快速生成项目结构。
创建SpringBoot项目教程,本文就不过多讲解了,具体操作可参考往期博文:
IDEA创建SpringBoot项目教程,讲解超详细(2024最新)!!!
3. Maven依赖引入
在Spring Boot项目中使用@Scheduled注解实现定时任务时,你通常不需要额外导入特定的依赖,因为@Scheduled是Spring框架的核心功能之一,并且Spring Boot会自动配置与调度相关的组件。
但是,确保你的Spring Boot项目包含了Spring Boot的起步依赖(starter dependencies),特别是spring-boot-starter或与你项目相关的特定starter依赖(比如spring-boot-starter-web用于Web应用),这些starter依赖会包含使用@Scheduled所需的所有必要组件。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
4. 创建定时任务
@Slf4j
@Component
@EnableScheduling
public class DemoTask {
// 每10秒执行一次,cron的表达式暂时不多说明了,可以自行百度学习
@Scheduled(cron = "0/10 * * * * ? ")
public void testSchedule() {
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
}
}
注释讲解
@Slf4j:Lombok库提供的一个注解,用于在Java类中自动生成一个名为log的日志对象。通过使用@Slf4j注解,你可以直接在代码中调用log对象的方法来记录不同级别的日志,如log.info(), log.debug(), log.error() 等。这些日志方法可以帮助你记录应用程序的运行状态、调试信息、警告和错误等,从而有助于排查问题、跟踪程序运行情况,提高系统稳定性和可维护性。
@Component:Spring框架中的一个核心注解,它用于将一个类标记为Spring上下文中的一个组件。当一个类被标记为@Component时,Spring容器会在启动时自动扫描并实例化这个类,并将其注册到Spring上下文中。这个类就成为了Spring上下文中的一个bean,可以被其他bean通过依赖注入的方式使用。
@EnableScheduling:Spring框架提供的一个注解,用于启用基于注解的定时任务调度功能。当在Spring的配置类(如使用@Configuration注解的类)上使用@EnableScheduling注解时,Spring会自动配置一个任务调度器(TaskScheduler),负责管理所有带有@Scheduled注解的方法。
@Scheduled:Spring框架中用于定时任务调度的注解。它允许开发者将一个方法标记为定时任务,并配置任务的执行时间间隔或Cron表达式,从而实现在指定时间或按照指定周期自动执行该方法的功能。 除了配置Cron表达式外,还可以通过fixedRate和fixedDelay两种方式设置定时任务,这两种方式可以自行了解。
执行结果
按照上面代码中给定的cron表达式@Scheduled(cron = "0/10 * * * * ? ")每十秒执行一次,那么最近几次的执行结果应当为:
5. 问题:执行时间延迟和单线程执行
如果定时任务中是执行非常快的任务的,时间非常非常短,按照上述方法实现定时任务,确实不会有什么的延迟性,但真实的业务场景中,业务的执行时间可能远比这里时间长。
例如:主动让线程睡上20秒,让我们再来看看输出结果是如何的吧。
@Slf4j
@Component
@EnableScheduling
public class DemoTask {
// 每10秒执行一次,cron的表达式暂时不多说明了,可以自行百度学习
@Scheduled(cron = "0/10 * * * * ? ")
public void testSchedule() {
try {
Thread.sleep(20000); // 休眠20秒,模拟业务场景执行时间
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果
- 执行时间延迟:从时间上可以明显看出,不再是每10秒执行一次,执行时间延迟很多,造成任务的。
- 单线程执行:从始至终都只有一个线程在执行任务,造成任务的堵塞。
5.1 问题原因
- 任务阻塞:定时任务设置了每10秒执行一次,但任务实际执行了20秒,那么下一个任务的执行就会因为前一个任务尚未完成而被阻塞。
- 单线程调度器:当使用@EnableScheduling默认配置创建定时任务时,通常Spring会使用一个单线程的TaskScheduler来执行这些定时任务。在默认配置下,这个单线程的调度器会顺序地执行每一个任务,而不会并行处理。
归根到底,线程阻塞式执行,执行任务线程数量过少 ~
5.2 解决方式
方式一:执行逻辑改为异步
首先我们先配置一个线程池,参数自己设置即可,我这里比较随意。
@Configuration
public class DemoTheadPoolConfig {
@Bean(name = "TaskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//设置核心线程数
executor.setCorePoolSize(10);
//设置最大线程数
executor.setMaxPoolSize(20);
//缓冲队列200:用来缓冲执行任务的队列
executor.setQueueCapacity(200);
//线程活路时间 60 秒
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("demo-thread-");
//设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
然后在定时任务中的将模拟业务的休眠20秒,改造成多线程并发的方式执行。
@Scheduled(cron = "0/10 * * * * ? ")
public void testSchedule() {
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(20000); // 休眠20秒,模拟业务场景执行时间
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
} catch (Exception e) {
e.printStackTrace();
}
}, taskExecutor);
}
执行结果
可以看到虽然业务执行时间仍然是20秒,但是10秒的定时任务没有再出现延迟执行的情况。
方式二:异步定时任务
异步定时任务其实和上面的方式原理是一样的,不过实现稍稍不同罢了,在定时任务的类上再加一个@EnableAsync注解,给方法添加一个@Async即可。
注意:
- @EnableAsync:开启异步任务
- @Async:给希望异步执行的方法标注
- 一般使用@Async都会指定线程池,比如写成这样@Async(value = "TaskExecutor")
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class DemoTask {
// 每10秒执行一次,cron的表达式暂时不多说明了,可以自行百度学习
@Async("TaskExecutor")
@Scheduled(cron = "0/10 * * * * ? ")
public void testSchedule() {
try {
Thread.sleep(20000); // 休眠20秒,模拟业务场景执行时间
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId()); // 日志输出
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果
结果显而易见也是和第一种方式一样的。
至此,我们的SpringBoot实现定时任务项目完美竣工!!!
ps:本教程我所阐述的这种方式,只能说适用于简单的单体项目,实际业务比这要复杂的多,如分布式场景还需考虑定时任务在多个机器下运行的问题,后续作者也会针对该问题出一篇分布式锁的教程,如感兴趣关注点一下吧!!!
有什么问题都可以评论区留言,看见都会回复的!!!
如果你觉得本篇文章对你有所帮助的,多多支持!!!
点赞收藏评论,抱拳了!!!