Spring Boot 定时任务

定时任务是指系统在固定的时间或者时间间隔,执行指定的任务。

 

1. 定时任务的实现方式

1)crontab

在 Unix/Linux 系统经常使用 crontab 命令实现定时任务。使用 contab 命令设置固定或者周期性任务后,由系统进程 crond 定期检查是否有要执行的工作,如果有要执行的工作便会自动执行该工作。

其它方式实现的定时任务,通常都会借鉴 crontab 命令的格式。

2)Timer

Timer 是指 Java 自带的 java.util.Timer 类,这个类允许调度一个 java.util.TimerTask 任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。

3)ScheduledExecutorService

ScheduledExecutorService 也是 JDK 自带的一个类。它基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行。也就是说,任务是并发执行,互不影响。

4)Spring Task

Spring Task,可以将它看成一个轻量级的 Quartz,而且使用起来比 Quartz 简单许多。

5)Quartz

Quartz 是一个功能比较强大的的调度器,可以让程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂。

 

2. 基于 Spring Boot 的定时任务

使用 Spring Boot 自带的定时任务,只需要添加相应的注解就可以实现。

1)导入SpringBoot启动包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>

2)启动类启用定时

在启动类里加上 @EnableScheduling 即可开启定时。

@SpringBootApplication

// 开启定时
@EnableScheduling 
public class SpringBootDemoTimeTaskApplication {

    private static final Logger logger = LoggerFactory.getLogger(SpringBootDemoTimeTaskApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemoTimeTaskApplication.class);
        logger.info("SpringBootDemoTimeTaskApplication start!");
    }
}   

3)创建定时任务实现类

编写定时任务实现类 ScheduleTask,有两个定时任务 task1 和 task2,分别对应计数器 count1 和 count2,每 1 秒输出一次结果。

package com.example.demo;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class ScheduleTask {
    // 计数器
    int count1 = 1;
    int count2 = 1;

    // 每隔一秒执行一次
    @Scheduled(fixedRate = 1000)
    public void task1() {
        System.out.println(Thread.currentThread().getName() + " [task1] 每秒执行一次, 执行第 [" + count1 + "] 次");
        count1++;
    }

    // 每隔一秒执行一次
    @Scheduled(fixedRate = 1000)
    public void task2() {
        System.out.println(Thread.currentThread().getName() + " [task2] 每秒执行一次, 执行第 [" + count2 + "] 次");
        count2++;
    }
}

运行结果:

scheduling-1 [task2] 每秒执行一次, 执行第 [1] 次
scheduling-1 [task1] 每秒执行一次, 执行第 [1] 次
scheduling-1 [task1] 每秒执行一次, 执行第 [2] 次
scheduling-1 [task2] 每秒执行一次, 执行第 [2] 次
scheduling-1 [task1] 每秒执行一次, 执行第 [3] 次
scheduling-1 [task2] 每秒执行一次, 执行第 [3] 次
scheduling-1 [task1] 每秒执行一次, 执行第 [4] 次
scheduling-1 [task2] 每秒执行一次, 执行第 [4] 次
...

4)执行时间的配置

在上面的定时任务中,我们在方法上使用 @Scheduled 注解来设置任务的执行时间。常用有三种属性配置方式:

  • fixedRate:定义一个按一定频率执行的定时任务。
  • fixedDelay:定义一个按一定频率执行的定时任务,与上面不同的是,改属性可以配合initialDelay, 定义该任务延迟执行时间。
  • cron:通过 crontab 格式的表达式来配置任务执行时间。

 

3. 多线程执行定时任务

Spring Boot 执行定时任务默认为单线程,可以看到两个定时任务都已经执行,并且使同一个线程中(scheduling-1)串行执行,如果只有一个定时任务,这样做肯定没问题,当定时任务增多,如果一个任务卡死,会导致其他任务也无法执行。单线程方式中,多个任务会相互影响。

我们可以将定时任务改造为多线程执行方式。

1)多线程配置类 AsyncConfig

增加一个多线程配置类 AsyncConfig。

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

// 表明该类是一个配置类
@Configuration

// 开启异步事件的支持
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor c() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setQueueCapacity(20);

        return taskExecutor;
    }
}

2)在定时任务的类或者方法上添加 @Async

package com.example.demo;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class ScheduleTask {
    // 计数器
    int count1 = 1;
    int count2 = 1;

    // 每隔一秒执行一次
    @Scheduled(fixedRate = 1000)

    // 使用异步执行
    @Async
    public void task1() {
        System.out.println(Thread.currentThread().getName() + " [task1] 每秒执行一次, 执行第 [" + count1 + "] 次");
        count1++;
    }

    // 每隔一秒执行一次
    @Scheduled(fixedRate = 1000)

    // 使用异步执行
    @Async
    public void task2() {
        System.out.println(Thread.currentThread().getName() + " [task2] 每秒执行一次, 执行第 [" + count2 + "] 次");
        count2++;
    }
}

此时,可让每一个任务都是在不同的线程中,启动项目,日志打印如下:

SimpleAsyncTaskExecutor-2 [task2] 每秒执行一次, 执行第 [1] 次
SimpleAsyncTaskExecutor-1 [task1] 每秒执行一次, 执行第 [1] 次
SimpleAsyncTaskExecutor-3 [task1] 每秒执行一次, 执行第 [2] 次
SimpleAsyncTaskExecutor-4 [task2] 每秒执行一次, 执行第 [2] 次
SimpleAsyncTaskExecutor-5 [task1] 每秒执行一次, 执行第 [3] 次
SimpleAsyncTaskExecutor-6 [task2] 每秒执行一次, 执行第 [3] 次
SimpleAsyncTaskExecutor-7 [task1] 每秒执行一次, 执行第 [4] 次
SimpleAsyncTaskExecutor-8 [task2] 每秒执行一次, 执行第 [4] 次
...

日志打印证明使用了多线程执行定时任务。

 

4. 定时任务设置方式

fixedDelay 和 fixedRate,单位使用毫秒。fixedRate:定义一个按一定频率执行的定时任务。fixedDelay:定义一个按一定频率执行的定时任务。

cron:通过 crontab 格式的表达式来配置任务执行时间。

cron 表达式有专门的语法,cron 共有 7 位,但是最后一位是年,可以留空,所以我们可以写 6 位:

* 第一位,表示秒,取值0-59
* 第二位,表示分,取值0-59
* 第三位,表示小时,取值0-23
* 第四位,日期天/日,取值1-31
* 第五位,日期月份,取值1-12
* 第六位,星期,取值1-7,星期一,星期二...,注:不是第1周,第二周的意思
          另外:1表示星期天,2表示星期一。
* 第7为,年份,可以留空,取值1970-2099

cron 中,还有一些特殊的符号,含义如下:

(*)星号:可以理解为每的意思,每秒,每分,每天,每月,每年...
(?)问号:问号只能出现在日期和星期这两个位置,表示这个位置的值不确定,每天3点执行,所以第六位星期的位置,我们是不需要关注的,就是不确定的值。同时:日期和星期是两个相互排斥的元素,通过问号来表明不指定值。比如,1月10日,比如是星期1,如果在星期的位置是另指定星期二,就前后冲突矛盾了。
(-)减号:表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12
(,)逗号:表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一,星期二,星期四
(/)斜杠:如:x/y,x是开始值,y是步长,比如在第一位(秒) 0/15就是,从0秒开始,每15秒,最后就是0,15,30,45,60    另:*/y,等同于0/y

下面列举几个例子:

0 0 3 * * ?     每天3点执行
0 5 3 * * ?     每天3点5分执行
0 5 3 ? * *     每天3点5分执行,与上面作用相同
0 5/10 3 * * ?  每天3点的 5分,15分,25分,35分,45分,55分这几个时间点执行
0 10 3 ? * 1    每周星期天,3点10分 执行,注:1表示星期天    
0 10 3 ? * 1#3  每个月的第三个星期,星期天 执行,#号只能出现在星期的位置

Spring Boot 简化了 Spring 应用的创建、运行、调试、部署等一系列问题。Spring Boot 主配置文件默认为 application.yml 或者 application.properties,我们可以根据自己的使用习惯,任取两种格式的文件之一即可。读取配置文件的方式有三种:@Value 注解、@ConfigurationProperties 注解 和 Environment 对象。