查看原文
其他

为什么你的 Spring Task 定时任务没有定时执行?

ImportNew 2021-03-11

The following article is from 侠梦的开发笔记 Author 侠梦

(给ImportNew加星标,提高Java技能)

来自作者投稿 作者:侠梦

前言

定时任务的使用,在开发中可谓是家常便饭了,定时发送邮件、短信。避免数据库,数据表过大,定时将数据转储。通知、对账等等场景。

当然实现定时任务的方式也有很多,比如使用 linux下的 crontab 脚本,jdk 中自带的 Timer 类。Spring Task或是 Quartz 。

相信你也有过如下的疑问:

  • Spring Task 的 crontab 的表达式 和linux下的 crontab 有什么区别?
  • crontab 表达式记不住?
  • 定时任务阻塞会有什么影响?
  • 多个定时任务的情况下如何运行的?
  • 具有相同表达式的定时任务,他们的执行顺序如何?
  • 为什么async异步任务没有生效?

所以这篇文章,我们来介绍一下,在 Spring Task 中, 定时任务的执行原理及相关问题。演示环境为 Spring Boot 项目

SpringBoot 定时任务的原理

相信绝大部分开发者都使用过 Spring Boot ,它为我们提供的 Starter 包含了定时任务的注解。所以我们来主要介绍一下 Spring Boot 实现定时任务的原理,和其相关注解的作用。

Spring 在 3.0版本后通过 @Scheduled 注解来完成对定时任务的支持。


在使用时,需要在Application 启动类上加上 @EnableScheduling 注解,它是从Spring 3.1后开始提供的。



由于Spring3 版本较低,使用得比较少了,我们使用高版本可能并不会考虑太多细节,大多只需要关注目标实现,所以我们在配套使用两个注解的时候,并不会出现什么问题。


在3.0 中 ,是通过   

<!-- 配置任务线性池 -->
<!-- 任务执行器线程数量 -->
<task:executor id="executor" pool-size="3" />
<!-- 任务调度器线程数量 -->
<task:scheduler id="scheduler" pool-size="3" />
<!-- 启用annotation方式 -->
<task:annotation-driven scheduler="scheduler"
executor="executor" proxy-target-class="true" />


上述的 XML 配置 和 @Scheduled 配合实现定时任务的,而我们这里的 @EnableScheduling 作用其实和它类似,主要用来发现注解了 @Scheduled 的方法,没有这个注解光有 @Scheduled  是无法执行的,大家可以做一个简单案例测试一下。

其底层是 Spring 自己实现的一套定时任务的处理逻辑,所以使用起来比较简单。

任务一直阻塞会怎么样?


介绍了两个注解的作用后,我们来开始做实验,简单的写一个定时执行的方法。



每隔 20s 输出一句话,在控制台输出几行记录后,打上了一个断点。

这样做,对后续的任务有什么影响呢?


可以看到,断点时的后续任务是阻塞着的,从图上,我们还可以看出初始化的名为pool-1-thread-1 的线程池同样证实了我们的想法,线程池中只有一个线程,创建方法是:
Executors.newSingleThreadScheduledExecutor();

从这个例子来看,断点时,任务会一直阻塞。当阻塞恢复后,会立马执行阻塞的任务。线程池内部时采用 DelayQueue 延迟队列实现的,它的特点是:无界、延迟、阻塞的一种队列,能按一定的顺序对工作队列中的元素进行排列。

多个定时任务的执行

通过上面的实验,我们知道,默认情况下,任务的线程池,只会有一个线程来执行任务,如果有多个定时任务,它们也应该是串行执行的。


从上图可以看出,一旦线程执行任务1后,就会睡眠2分钟。线程在死循环内部一直处于Running 状态。


通过观察日志,根本没有任务2的输出,所以得知,这种情况下,多个定时任务是串行执行的,类似于多辆车通过单行道的桥,如果一辆车出现阻塞,其他的车辆都会受到影响。

那如果线程池包含多个线程的情况下,多个定时任务并发的情况是什么样?


串行当然很好理解,就是上文说的汽车过桥,依次通过。再来理解并发,区别于并行,并发是指一个处理器同时处理多个任务,而并行是指多个(核)处理器同时处理多个不同的任务。并发不一定同一时间发生,而并行,指的是同一时间。


相同表达式的定时任务,执行顺序如何?

从上面的实验同样能知道,具有相同表达式的定时任务,还是和调度有关,如果是默认的线程池,那么会串行执行,首先获取到 cpu 时间片的先执行。在多线程情况下,具体的先后执行顺序和线程池线程数和所用线程池所用队列等等因素有关。


Spring Task和linux crontab的cron语法区别?


两者的 cron 表达式其实很相似,需要注意的是 linux 的 crontab 只为我们提供了最小颗粒度为分钟级的任务,而 java 中最小的粒度是从秒开始的。具体细节如下图:



在cron语法中容易犯的错误
以spring 中的 task 为例,cron 表达式中 "/" 代表每的意思,“*/10”表示每10个单位。

在 cron 语法 中很多人会犯错误。比如要求写出每十分钟定时执行的 cron 语句,可能会有以下版本的出现:


绿色的为正确的写法,在我们写完 cron 表达式的时候,可以适当的调低执行间隔时间来测试,或是通过一些在线的网站来检测你的cron脚本是否正确。


@Async异步注解原理及作用
Spring task中 和异步相关的注解有两个 , 一个是 @EnableAsync ,另一个就是 @Async 。


首先我们单纯的在方法上引入 @Async 异步注解,并且打印当前线程的名称,实验后发现,方法仍然是由一个线程来同步执行的。

和 @schedule  类似 还是通过 @Enable 开头的注解来控制执行的。我们在启动类上加入@EnableAsync 后再观察输出内容。


默认情况下,其内部是使用的名为SimpleAsyncTaskExecutor的线程池来执行任务,而且每一次任务调度,都会新建一个线程。

使用 @EnableAsync 注解开启了 Spring 的异步功能,Spring 会按照如下的方式查找相应的线程池用于执行异步方法:

查找实现了TaskExecutor接口的Bean实例。

如果上面没有找到,则查找名称为taskExecutor并且实现了Executor接口的Bean实例。

如果还是没有找到,则使用SimpleAsyncTaskExecutor,该实现每次都会创建一个新的线程执行任务。

并发执行任务如何配置?


方式一,我们可以将默认的线程池替换为我们自定义的线程池。通过 ScheduleConfig 配置文件实现 SchedulingConfigurer 接口,并重写 setSchedulerfang 方法。


可实现 AsyncConfigurer 接口复写 getAsyncExecutor 获取异步执行器,getAsyncUncaughtExceptionHandler 获取异步未捕获异常处理器   

@Configurationpublic
class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}


方式二:不改变任务调度器默认使用的线程池,而是把当前任务交给一个异步线程池去执行。   
@Scheduled(fixedRate = 1000*10,initialDelay = 1000*20)
@Async("hyqThreadPoolTaskExecutor")
public void test(){
System.out.println(Thread.currentThread().getName()+"--->xxxxx--->"+Thread.currentThread().getId());
}

//自定义线程池
@Bean(name = "hyqThreadPoolTaskExecutor")
public TaskExecutor getMyThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(200);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("hyq-threadPool-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setAwaitTerminationSeconds(60);
taskExecutor.initialize();
return taskExecutor;
}


其他问题


如果是定时任务没有生效,需要检查 @EnableScheduling 注解是否加上。如果是异步没有生效,需要检查 @EnableAsync 注解是否加上,并且定义线程池,否则仍然是串行执行的。


总结


文章介绍了SpringBoot 定时任务的原理, 3.0 版本前后的区别,通过单线程任务阻塞实验,探究了延迟队列及串行、并行、并发的概念。对比了linux下的 crontab 和spring的cron 表达式区别以及常犯的错误。最后通过实验异步注解,两种方式配置线程池,让任务高效运作。



推荐阅读  点击标题可跳转

改进Spring Boot REST API错误处理

SpringBoot+RabbitMQ (保证消息100%投递成功并被消费)

Spring Cloud与Dubbo优缺点详解


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

好文章,我在看❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存