SpringBoot系列(六):使用SpringBoot定时任务时不得不采的坑


作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

摘要:本文我们将分享介绍如何基于Spring Boot搭建的项目使用Spring Task定时任务,带领各位小伙伴踩一踩在使用Spring定时任务时所出现的坑,并采用线程池~多线程任务调度的形式对出现的坑加以解决、优化!

内容:对于定时任务,相信各位小伙伴都有所耳闻,甚至有些小伙伴对定时任务的使用已经到了“炉火纯青”的地步!而事实上,在实际的项目、特别是企业级Spring的项目开发中, Spring Task定时任务的使用是相当频繁的。

本文我们将基于前文Spring Boot搭建的标准企业级项目作为奠基,采用注解的形式将Spring Task定时任务应用到项目中,那废话不多讲,咱们直接进入撸码环节!

(1)首先,我们在 com.debug.springboot.server 包目录下建立scheduler包目录,并在其下建立一个通用化的用于编写定时任务的CommonScheduler 类,如下源代码所示,我们建立了三个定时任务,其中每个定时任务执行的时间频率分别为每5s、每6s、每7s执行一次:

/**
* spring task-定时任务调度
* @Author:debug (SteadyJack)
* @Date: 2019/9/7 11:05
**/
@Component
public class CommonScheduler {
private static final Logger log= LoggerFactory.getLogger(CommonScheduler.class);

//定时任务1
@Scheduled(cron = "0/5 * * * * *")
public void schedulerOne(){
log.info("---执行定时任务1---");
}

//定时任务2
@Scheduled(cron = "0/6 * * * * *")
public void schedulerTwo(){
log.info("---执行定时任务2---");

try {
//模拟当前定时任务每次执行业务逻辑时需要花费的时间 3s
Thread.sleep(3000);
}catch (Exception e){e.printStackTrace();}
}

//定时任务3
@Scheduled(cron = "0/7 * * * * *")
public void schedulerThree(){
log.info("---执行定时任务3---");

try {
//模拟当前定时任务每次执行业务逻辑时需要花费的时间 4s
Thread.sleep(4000);
}catch (Exception e){e.printStackTrace();}
}
}

(2)其中,为了更好的模拟在实际项目开发中 定时任务 执行的业务逻辑,我们假定了每次执行定时任务2时需要花费3s的时间,定时任务3执行业务逻辑时需要花费4s的时间。

理论上,每个定时任务在执行相应的业务逻辑时,是不应该相互影响的,即在理想的情况下,SchedulerOne应当每隔5s执行一次业务逻辑,SchedulerTwo应当每隔6s执行一次业务逻辑,SchedulerThree应当每隔7s执行一次业务逻辑,以此类推。

下面,我们将整个项目运行起来,即可触发这几个定时任务调度的执行。当然,在此之前,我们需要在MainApplication启动类中加入一个注解:@EnableScheduling 即允许定时任务调度的执行,如下图所示:


  (3)完了之后,即可将整个项目运行起来了,耐心观察控制台Console的输出信息,即可看到每个定时任务的执行频率,如下图所示:  


(4)从该控制台Console中,我们可以看到几点信息:

A.第一点是所有的定时任务确实都已经执行了;

B.第二点是每个定时任务虽然都已经执行了,但是却不是按照设定的cron来执行,特别是SchedulerOne定时任务1,从上图中会发现第一次执行时是在 12:07:27 ,下次执行时却是在 12:07:35 ,前后间距为8s,而不是5s,这一点在我们看来是不正常的;

C.除此之外,还有最后一点,即所有定时任务调度的执行竟然都是在同一个“线程”内执行,这是很不可思议的,因为这会导致那些“cron相隔间距很短” 的定时任务出现堵塞的现象,这一点其实就是上述 B第二点出现的现象 的原因所在!

因此我们所讲的 定时任务 在使用的过程中出现的坑,其实就是上述所讲的第二点B跟第三点C,而为了解决这样的问题(坑),下面我们将采用“线程池~多线程”的形式配置定时任务调度的执行策略。

(1)首先,我们在 com.debug.springboot.server 包目录下建立 config 包,然后在该包目录中建立 “定时任务调度-线程池的通用配置类” SchedulerConfig ,其源代码如下所示:

/**
* 定时任务调度-线程池配置
* @Author:debug (SteadyJack)
* @Date: 2019/9/7 11:12
**/
@Configuration
public class SchedulerConfig {

//任务调度线程池配置
@Bean("taskExecutor")
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
//核心线程数
executor.setCorePoolSize(4);
//最大核心线程数
executor.setMaxPoolSize(10);
//设置队列中等待被调度的任务的数量
executor.setQueueCapacity(8);
executor.initialize();
return executor;
}
}

  (2)完了之后,需要在通用的定时任务调度类中加入 该 taskBean 的使用,即主要加入两个注解 @EnableAsync (允许异步执行)、@Async("taskExecutor") (基于指定的配置Bean异步执行相应的业务逻辑),调整后的源代码如下所示:  

/**
* spring task-定时任务调度
* @Author:debug (SteadyJack)
* @Date: 2019/9/7 11:05
**/
@Component
@EnableAsync
public class CommonScheduler {

private static final Logger log= LoggerFactory.getLogger(CommonScheduler.class);

//定时任务1
@Scheduled(cron = "0/5 * * * * *")
@Async("taskExecutor")
public void schedulerOne(){
log.info("---执行定时任务1---");
}

//定时任务2
@Scheduled(cron = "0/6 * * * * *")
@Async("taskExecutor")
public void schedulerTwo(){
log.info("---执行定时任务2---");

try {
//模拟当前定时任务每次执行业务逻辑时需要花费的时间 3s
Thread.sleep(3000);
}catch (Exception e){e.printStackTrace();}
}

//定时任务3
@Scheduled(cron = "0/7 * * * * *")
@Async("taskExecutor")
public void schedulerThree(){
log.info("---执行定时任务3---");

try {
//模拟当前定时任务每次执行业务逻辑时需要花费的时间 4s
Thread.sleep(4000);
}catch (Exception e){e.printStackTrace();}
}
}


(3)将整个项目运行起来,观察控制台Console的输出,会发现此时控制台的输出信息跟没使用“线程池~多线程”时是两种结果,如下图所示:


  至此,关于Spring Task定时任务调度在Spring Boot项目中的使用我们已经介绍、实战完毕了,实不相瞒,在实际的企业级应用开发中,加入“线程池~多线程”的任务配置形式才是使用@Scheduled定时任务调度的正确方式!  


补充:

1、本文涉及到的相关的源代码可以到此地址,check出来进行查看学习:

https://gitee.com/steadyjack/SpringBootTechnology

2、目前Debug已将本文所涉及的内容整理录制成视频教程,感兴趣的小伙伴可以前往观看学习:

https://www.fightjava.com/web/index/course/detail/5

3、关注一下Debug的技术微信公众号呗,最新的技术文章、技术课程以及技术专栏将会第一时间在公众号发布哦!