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

发布于:2024-05-07 ⋅ 阅读:(29) ⋅ 点赞:(0)


在日常开发中,定时任务的应用已经变得十分常见,例如定时发送邮件、短信,数据定时转储等。然而,有时候我们会遇到定时任务没有按照预期执行的情况。本文将讨论 Spring Task 中定时任务执行的原理以及可能遇到的相关问题。

SpringBoot 定时任务的原理

绝大部分开发者在使用 Spring Boot 时都会使用它提供的定时任务 Starter 和定时任务的注解。因此,让我们首先了解一下 Spring Boot 实现定时任务的原理以及相关注解的作用。

自 Spring 3.0 版本以后,Spring 使用 @Scheduled 注解来支持定时任务。

在使用时,我们需要在应用的启动类上加上 @EnableScheduling 注解,该注解从 Spring 3.1 开始提供。

然而,在 Spring 3.0 版本之前,我们通常会通过 XML 配置和 @Scheduled 注解配合使用来实现定时任务。而 @EnableScheduling 注解实际上相当于该 XML 配置的简化版。它的作用是发现标记了 @Scheduled 注解的方法,没有该注解的方法将不会被执行。但是在大多数情况下,我们只需要关注目标实现,而不必深究其背后的细节。

任务一直阻塞会怎么样?

为了更好地理解定时任务的执行过程,我们进行了一个简单的实验:编写一个每隔 20 秒执行一次的定时任务,并在其中加入一个断点。

实验结果显示,当遇到断点时,后续的任务会被阻塞。这是因为线程池中只有一个线程用于执行任务,而该线程被阻塞后,后续任务将无法执行。线程池内部采用 DelayQueue 延迟队列实现,其特点是无界、延迟、阻塞,能够按照一定的顺序执行工作队列中的元素。

多个定时任务的执行

根据前面的实验结果,我们知道默认情况下,任务的线程池只有一个线程,因此多个定时任务应该是串行执行的。如果一个任务被阻塞,其他任务也会受到影响。

那么,如果我们将线程池的线程数设置为多个,会发生什么呢?实验结果显示,即使线程池中包含多个线程,多个定时任务仍然是串行执行的。只有在使用自定义的线程池,并且在任务上加上 @Async 注解时,才能实现并发执行。

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

具有相同表达式的定时任务的执行顺序与调度有关。默认情况下,它们是串行执行的,首先获取到 CPU 时间片的任务会先执行。在多线程的情况下,具体的执行顺序取决于线程池的线程数、所用队列等因素。

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

两者的 cron 表达式非常相似,但需要注意的是,Linux 的 crontab 只提供了分钟级别的最小粒度,而 Spring Task 的最小粒度从秒开始。

在 cron 语法中容易犯的错误

在编写 cron 表达式时,很容易犯一些错误。例如,要求编写每十分钟执行一次的 cron 语句时,可能会写成类似以下的版本:

0 0/10 * * * ?

因此,在编写 cron 表达式后,建议适当降低执行间隔时间进行测试,或者使用在线工具检测 cron 脚本是否正确。

@Async 异步注解原理及作用

Spring Task 中与异步相关的注解有两个,一个是 @EnableAsync,另一个是 @Async

默认情况下,@Async 注解会使方法仍然由一个线程同步执行。要想实现异步执行,需要在启动类上加上 @EnableAsync 注解,并定义一个线程池。

并发执行任务如何配置?

我们可以通过两种方式来配置并发执行任务:

  1. 将默认的线程池替换为自定义的线程池。
  2. 不改变任务调度器默认使用的线程池,而是将任务交给一个异步线程池去执行。

通过这些方式,我们可以让任务高效地并发执行,提高系统的性能。

方式一,我们可以将默认的线程池替换为我们自定义的线程池。通过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 注解,并且定义了线程池,否则仍然是串行执行的。

总结

本文介绍了 Spring Boot 定时任务的原理,通过实验探究了延迟队列及串行、并行、并发的概念。对比了 Linux 下的 crontab 和 Spring 的 cron 表达式区别以及常见的错误。最后通过实验异步注解,两种方式配置线程池,使任务能够高效运行。希望本文能够对你有所帮助。