当前位置:   article > 正文

Spring Boot 3种定时任务方式_定时 timeunit

定时 timeunit

我是陈皮,一个在互联网 Coding 的 ITer,个人微信公众号「陈皮的JavaLib」关注第一时间阅读最新技术文章。

1 什么是定时任务

在项目开发过程中,经常会使用到定时任务。顾名思义,定时任务一般指定时执行的方法。例如,每天凌晨0点同步 A 系统的数据到 B 系统;每2小时统计用户的积分情况;每周一给支付宝用户推送上周收入支出数据报表等等。

一般情况下,很多业务会定时在凌晨进行处理。因为这能避开用户使用高峰期,空闲时服务器资源充足,而且对用户影响小。

通过 Spring Boot 框架,我们可以使用3种方式来实现定时任务。

  • 第1种是基于注解的方式,比较常用,但是这种在程序运行过程种不能动态更改定时任务的时间。
  • 第2种是可以动态更改定时任务的时间。
  • 第3种是可以动态更改定时任务的时间,还可以动态启动,停止定时任务。

2 项目依赖

项目依赖如下,主要是引入 Spring Boot 相关依赖。此采用 Gradle 项目。

plugins {
    id 'org.springframework.boot' version '2.6.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.chenpi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3 @Scheduled 定时任务

使用 Spring Boot 内置的注解方式,即在需要定时执行的方法上添加@Scheduled注解即可。定时执行的方法不能有参数,并且一般没有返回值,即使有返回值也没什么用。注意定时任务所在的类要作为 Spring Bean,在类上添加@Component注解即可。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  // 每5秒执行一次
  @Scheduled(cron = "0/5 * * * * ? ")
  public void test() {
    LOGGER.info(">>>>> ScheduledTask doing ...");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

然后在启动类上添加@EnableScheduling注解开启定时任务。默认情况下,系统会自动启动一个线程,调度执行项目中定义的所有定时任务。

package com.chenpi.springschedule;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

启动项目,即可在控制台中每5秒看到定时任务被执行。

2022-04-11 20:46:55.011  INFO 11800 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 20:47:00.014  INFO 11800 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 20:47:05.003  INFO 11800 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 20:47:10.003  INFO 11800 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 20:47:15.001  INFO 11800 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
  • 1
  • 2
  • 3
  • 4
  • 5

@Scheduled注解源码如下,它有以下几个属性,使用时至少要指定cronfixedDelayfixedRat3个属性中的一个。接下来对每一个属性进行详解。

package org.springframework.scheduling.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

	String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;

	String cron() default "";

	String zone() default "";

	long fixedDelay() default -1;

	String fixedDelayString() default "";

	long fixedRate() default -1;

	String fixedRateString() default "";

	long initialDelay() default -1;

	String initialDelayString() default "";

	TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

3.1 cron

cron 的值是一个 cron 表达式字符串,指明定时任务的执行时机。

如果它的值是一个特殊的-字符串即CRON_DISABLED属性定义的值,代表定时任务无效,不会执行。当 cron 值使用占位符${...}引用外部值时,可以修改外部值(例如配置文件)为-来灵活控制定时任务的启停。

String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED; // 值为-

String cron() default "";
  • 1
  • 2
  • 3

使用 cron 属性的方式最常用,因为它能涵盖各种时间的配置。而且有许多在线 cron 表达式生成网站,例如:https://www.bejson.com/othertools/cron/,如下所示:

在这里插入图片描述

例如时一个定时任务,每5秒执行1次。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  // 每5秒执行一次
  @Scheduled(cron = "0/5 * * * * ? ")
  public void test() {
    LOGGER.info(">>>>> ScheduledTask doing ...");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

cron 的值可以使用占位符${...}来引用外部指定的值,例如引用配置文件中定义的变量,如下所示:

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  @Scheduled(cron = "${cron.exp}")
  public void test() {
    LOGGER.info(">>>>> ScheduledTask doing ...");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在配置文件application.properties中填写配置变量的值。此种方式比较灵活,不用修改代码即可更改定时任务的时间。而且如果将值改为-,则代表定时任务无效。

cron.exp=0/5 * * * * ?
#cron.exp=-
  • 1
  • 2

3.2 fixedDelay

此属性表明,上一次定时任务执行完后,延迟多久再次执行定时任务。默认以毫秒为单位,也可以配合timeUnit属性设置其他时间单位。

long fixedDelay() default -1;

TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
  • 1
  • 2
  • 3

以下例子代表,上一次定时任务执行完后,延迟 1000 ms 再执行定时任务。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  @Scheduled(fixedDelay = 1000)
  public void test() {
    LOGGER.info(">>>>> ScheduledTask doing ...");
    try {
      // 休眠2秒
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

输出结果如下,刚好两次执行时间间隔3秒(2秒休眠+1秒延迟)。

2022-04-11 21:18:27.796  INFO 14700 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:18:30.812  INFO 14700 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:18:33.825  INFO 14700 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:18:36.841  INFO 14700 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:18:39.860  INFO 14700 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:18:42.878  INFO 14700 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.3 fixedDelayString

此属性表明,上一次定时任务执行完后,延迟多久再次执行定时任务。默认以毫秒为单位,也可以配合timeUnit属性设置其他时间单位。作用和 fixedDelay 一样,只不过 fixedDelayString 值是字符串的形式,而且支持占位符。

String fixedDelayString() default "";
    
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
  • 1
  • 2
  • 3

以下例子代表,上一次定时任务执行完后,延迟 1000 ms 再执行定时任务。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  @Scheduled(fixedDelayString = "1000")
  public void test() {
    LOGGER.info(">>>>> ScheduledTask doing ...");
    try {
      // 休眠2秒
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

输出结果如下,刚好两次执行时间间隔3秒(2秒休眠+1秒延迟)。

2022-04-11 21:30:06.547  INFO 14396 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:30:09.561  INFO 14396 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:30:12.583  INFO 14396 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:30:15.602  INFO 14396 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
  • 1
  • 2
  • 3
  • 4

使用占位符如下所示,并且在配置文件application.properties中指定配置变量的值。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  @Scheduled(fixedDelayString = "${fixed.delay}")
  public void test() {
    LOGGER.info(">>>>> ScheduledTask doing ...");
    try {
      // 休眠2秒
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
fixed.delay=1000
  • 1

3.4 fixedRate

此属性表明,两次定时任务调用之间间隔的时间,默认以毫秒为单位,也可以配合timeUnit属性设置其他时间单位。即上一个调用开始后再次调用的延迟时间(不用等上一次调用完成)。

long fixedRate() default -1;

TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
  • 1
  • 2
  • 3

Spring Boot 默认情况下是使用单个线程是来执行所有定时任务的,所以即使前一个调用还未执行完,下一个调用可以开始了,那它也得等上一个调用执行完了,才能执行。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  @Scheduled(fixedRate = 1000)
  public void test() {
    try {
      // 休眠5秒
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    LOGGER.info(">>>>> ScheduledTask doing ...");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

上述两次定时任务调用之间间隔为1秒,但是执行时间为5秒,但是发现它们间隔执行时间还是5秒,而且打印出的都是同一个线程名 TaskScheduler-1,证明了默认情况下使用单个线程来执行所有定时任务。

2022-04-11 21:36:26.202  INFO 8984 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:36:31.214  INFO 8984 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:36:36.219  INFO 8984 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:36:41.226  INFO 8984 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
  • 1
  • 2
  • 3
  • 4

而且,如果项目中有多个不同的定时任务,单个线程情况下,这多个定时任务是串行执行的。如下所示,有2个定时任务。

@Component
public class ScheduledTask {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);
    
    @Scheduled(fixedRate = 1000)
    public void test() {
        try {
            // 休眠5秒
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LOGGER.info(">>>>> ScheduledTask doing ...");
    }
}


@Component
public class ScheduledTask01 {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask01.class);

  @Scheduled(fixedRate = 1000)
  public void test() {
    try {
      // 休眠5秒
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    LOGGER.info(">>>>> ScheduledTask01 doing ...");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

单线程情况下,结果输出如下所示,发现多个方法之间是串行执行的。本来同一个定时任务的相邻2次调用是5秒间隔,现在变成10秒了。

2022-04-11 21:38:22.795  INFO 12556 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:38:27.804  INFO 12556 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask01  : >>>>> ScheduledTask01 doing ...
2022-04-11 21:38:32.808  INFO 12556 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 21:38:37.813  INFO 12556 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask01  : >>>>> ScheduledTask01 doing ...
2022-04-11 21:38:42.818  INFO 12556 --- [   scheduling-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
  • 1
  • 2
  • 3
  • 4
  • 5

这种情况可以通过配置线程池的方式,进行多个线程执行,后面讲解。

3.5 fixedRateString

此属性表明,两次定时任务调用之间间隔的时间,默认以毫秒为单位,也可以配合timeUnit属性设置其他时间单位。即上一个调用开始后再次调用的延迟时间(不用等上一次调用完成)。与fixedRate相同,只不过值是字符串的形式,而且支持占位符。

String fixedRateString() default "";

TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
  • 1
  • 2
  • 3

3.6 initialDelay 和 initialDelayString

initialDelay属性表明,第一次执行fixedRatefixedDelay任务之前要延迟的时间,默认以毫秒为单位,也可以配合timeUnit属性设置其他时间单位。需配合 fixedDelay 或者 fixedRate 一起使用。其中 initialDelayString 值是字符串的形式,并且支持占位符。

long initialDelay() default -1;

String initialDelayString() default "";
  • 1
  • 2
  • 3
// 延迟3秒才开始执行第一次任务
@Scheduled(fixedDelayString = "1000", initialDelay = 3000)
public void test() {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    LOGGER.info(">>>>> ScheduledTask doing ...");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3.7 zone

时区,cron 表达式会基于该时区解析。默认是一个空字符串,即使用服务器所在地的时区。它的值是一个时区 ID,我们一般使用的时区是Asia/Shanghai。此属性一般默认即可。

String zone() default "";
  • 1
package com.chenpi.springschedule.task;

import java.util.Arrays;
import java.util.TimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  @Scheduled(cron = "0/5 * * * * ?", zone = "Asia/Shanghai")
  @Async("myExecutor")
  public void test() {
    TimeZone defaultTimeZone = TimeZone.getDefault();
    // >>>>> ScheduledTask doing ...Asia/Shanghai  
    LOGGER.info(">>>>> ScheduledTask doing ..." + defaultTimeZone.getID());
    // 打印出可取得的所有时区ID
    String[] availableIDs = TimeZone.getAvailableIDs();
    LOGGER.info(Arrays.toString(availableIDs));
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

4 多线程定时任务

使用@Scheduled注解形式的定时任务,默认是单个线程来执行项目中的所有定时任务的。即使如果同一时刻有两个定时任务需要执行,那么也只能其中一个定时任务完成之后再执行下一个定时任务。如果项目只有一个定时任务还好。如果定时任务增多时,如果一个任务被阻塞,则会导致其他任务无法正常执行。

可以配置任务调度线程池,来解决以上问题。首先配置一个线程池,如下所示:

package com.chenpi.springschedule.config;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

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

/**
* @author 陈皮
* @version 1.0
* @description 线程池配置
* @date 2021/3/2
*/
@Configuration
public class ExecutorConfig {
    
    public static final int CORE_POOL_SIZE = 2;
    public static final int MAX_POOL_SIZE = 8;
    public static final int QUEUE_CAPACITY = 100;
    
    @Bean("myExecutor")
    public Executor asyncServiceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数大小
        executor.setCorePoolSize(CORE_POOL_SIZE);
        // 最大线程数大小
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        // 阻塞队列容量
        executor.setQueueCapacity(QUEUE_CAPACITY);
        // 线程名前缀
        executor.setThreadNamePrefix("myTask-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

然后通过@Async注解添加到定时任务方法上。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 定时任务类
 * @date 2021/3/2
 */
@Component
public class ScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledTask.class);

  @Scheduled(fixedRate = 1000)
  @Async("myExecutor")
  public void test() {
    try {
      // 休眠5秒
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    LOGGER.info(">>>>> ScheduledTask doing ...");
  }

  @Scheduled(fixedRate = 1000)
  @Async("myExecutor")
  public void test01() {
    try {
      // 休眠5秒
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    LOGGER.info(">>>>> ScheduledTask01 doing ...");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

在启动类上添加@EnableAsync注解开启异步事件支持。

package com.chenpi.springschedule;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

最终输出结果如下,发现多个定时任务是由线程池中的不同线程来执行了。

2022-04-11 22:00:44.483  INFO 15668 --- [       myTask-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 22:00:44.483  INFO 15668 --- [       myTask-2] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask01 doing ...
2022-04-11 22:00:49.498  INFO 15668 --- [       myTask-2] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask01 doing ...
2022-04-11 22:00:49.498  INFO 15668 --- [       myTask-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 22:00:54.507  INFO 15668 --- [       myTask-2] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask doing ...
2022-04-11 22:00:54.507  INFO 15668 --- [       myTask-1] c.c.springschedule.task.ScheduledTask    : >>>>> ScheduledTask01 doing ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5 可动态更改时间的定时任务

此种方式要实现SchedulingConfigurer接口,并且重写configureTasks方法。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

/**
 * @author 陈皮
 * @version 1.0
 * @description 可动态更改时间的定时任务
 * @date 2021/3/2
 */
@Component
public class ChangeTimeScheduledTask implements SchedulingConfigurer {

  private static final Logger LOGGER = LoggerFactory.getLogger(ChangeTimeScheduledTask.class);

  // cron表达式,我们动态更改此属性的值即可更改定时任务的执行时间
  private String expression = "0/5 * * * * *";

  @Override
  public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    // 定时任务要执行的方法
    Runnable task = () -> LOGGER.info(">>> configureTasks ...");
    // 调度实现的时间控制
    Trigger trigger = triggerContext -> {
      CronTrigger cronTrigger = new CronTrigger(expression);
      return cronTrigger.nextExecutionTime(triggerContext);
    };
    taskRegistrar.addTriggerTask(task, trigger);
  }

  public String getExpression() {
    return expression;
  }

  public void setExpression(String expression) {
    this.expression = expression;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

然后我们编写一个接口进行调用,动态改变定时任务的时间。

package com.chenpi.springschedule.controller;

import com.chenpi.springschedule.task.ChangeTimeScheduledTask;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 陈皮
 * @version 1.0
 * @description
 * @date 2021/3/2
 */
@RestController
@RequestMapping("demo")
public class DemoController {

  private final ChangeTimeScheduledTask changeTimeScheduledTask;

  public DemoController(final ChangeTimeScheduledTask changeTimeScheduledTask) {
    this.changeTimeScheduledTask = changeTimeScheduledTask;
  }

  @GetMapping
  public String testChangeTimeScheduledTask() {
    changeTimeScheduledTask.setExpression("0/10 * * * * *");
    return "ok";
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

启动服务,没调用接口之前,定时任务是每5秒执行一次。然后我们调用接口,改变定时任务的时间,结果变为每10秒执行一次。

在这里插入图片描述

6 可动态启停定时任务

此种方式可以动态手动启动,停止定时任务,以及能动态更改定时任务的执行时间。

其原理是利用线程池实现任务调度,可以实现任务的调度和删除。借助ThreadPoolTaskScheduler线程池任务调度器,能够开启线程池进行任务调度。通过 ThreadPoolTaskScheduler 的schedule方法创建一个定时计划ScheduleFuture,ScheduleFuture 中有一个cancel方法可以停止定时任务。schedule 方法中有2个参数,一个是 Runnable task,线程接口类,即我们要定时执行的方法,另一个参数是Trigger trigger,定时任务触发器,带有 cron 值。

package com.chenpi.springschedule.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import java.util.concurrent.ScheduledFuture;

/**
 * @author 陈皮
 * @version 1.0
 * @description 可动态启停定时任务
 * @date 2021/3/2
 */
@Component
public class DynamicScheduledTask {

  private static final Logger LOGGER = LoggerFactory.getLogger(DynamicScheduledTask.class);

  private final ThreadPoolTaskScheduler threadPoolTaskScheduler;

  public DynamicScheduledTask(final ThreadPoolTaskScheduler threadPoolTaskScheduler) {
    this.threadPoolTaskScheduler = threadPoolTaskScheduler;
  }

  private ScheduledFuture<?> future;

  /**
   * 启动定时器
   */
  public void startTask() {
    // 第一个参数为定时任务要执行的方法,第二个参数为定时任务执行的时间
    future = threadPoolTaskScheduler.schedule(this::test, new CronTrigger("0/5 * * * * *"));
  }

  /**
   * 停止定时器
   */
  public void endTask() {
    if (future != null) {
      future.cancel(true);
    }
  }

  /**
   * 改变调度的时间,先停止定时器再启动新的定时器
   */
  public void changeTask() {
    // 停止定时器
    endTask();
    // 定义新的执行时间,并启动
    future = threadPoolTaskScheduler.schedule(this::test, new CronTrigger("0/10 * * * * *"));
  }

  /**
   * 定时任务执行的方法
   */
  public void test() {
    LOGGER.info(">>>>> DynamicScheduledTask ...");
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

最后编写接口,对启动,停止,更改时间进行调用即可。

package com.chenpi.springschedule.controller;

import com.chenpi.springschedule.task.DynamicScheduledTask;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 陈皮
 * @version 1.0
 * @description
 * @date 2021/3/2
 */
@RestController
@RequestMapping("demo")
public class DemoController {

  private final DynamicScheduledTask dynamicScheduledTask;

  public DemoController(final DynamicScheduledTask dynamicScheduledTask) {
    this.dynamicScheduledTask = dynamicScheduledTask;
  }

  @GetMapping("startDynamicScheduledTask")
  public String startDynamicScheduledTask() {
    dynamicScheduledTask.startTask();
    return "ok";
  }

  @GetMapping("endDynamicScheduledTask")
  public String endDynamicScheduledTask() {
    dynamicScheduledTask.endTask();
    return "ok";
  }

  @GetMapping("changeDynamicScheduledTask")
  public String changeDynamicScheduledTask() {
    dynamicScheduledTask.changeTask();
    return "ok";
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

启动服务,因为没有调用启动定时器接口,所以定时任务不会执行。只有调用了启动的接口,定时任务才开始执行。在服务运行期间,可任意进行定时任务的开启,停止和更改时间操作。

2022-04-11 22:33:35.006  INFO 2356 --- [   scheduling-1] c.c.s.task.DynamicScheduledTask          : >>>>> DynamicScheduledTask ...
2022-04-11 22:33:40.004  INFO 2356 --- [   scheduling-1] c.c.s.task.DynamicScheduledTask          : >>>>> DynamicScheduledTask ...
// 此调用停止计时器,暂停了30秒之后再调用更改接口
2022-04-11 22:34:10.016  INFO 2356 --- [   scheduling-1] c.c.s.task.DynamicScheduledTask          : >>>>> DynamicScheduledTask ...
2022-04-11 22:34:20.011  INFO 2356 --- [   scheduling-1] c.c.s.task.DynamicScheduledTask          : >>>>> DynamicScheduledTask ...
2022-04-11 22:34:30.005  INFO 2356 --- [   scheduling-1] c.c.s.task.DynamicScheduledTask          : >>>>> DynamicScheduledTask ...
2022-04-11 22:34:40.004  INFO 2356 --- [   scheduling-1] c.c.s.task.DynamicScheduledTask          : >>>>> DynamicScheduledTask ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

7 分布式集群注意事项

虽然 Scheduled Task 是一种轻量级的任务定时调度器,相比于Quartz减少了很多的配置信息。但是 Scheduled Task 的有个缺点是不适用于分布式集群的操作,因为集群的节点之间是不会共享任务信息的,会导致在多个服务器上执行相同重复的定时任务。

如果在多个服务器上执行相同的定时任务,对你的业务不影响那还好。但有些业务不允许重复执行,那我们可以通过分布式锁,只让其中一个拿到锁的节点来执行定时任务。类似如下:

@Scheduled(cron = "${cron.exp}")
public void test() {
    String lockKey = RedisKeyUtil.genKey(RedisKeyUtil.SCHEDULED_TASK_LOCK);
    boolean lockSuccess = redisUtils.getLock(lockKey, "1", 30000);
    if (!lockSuccess) {
        LOGGER.warn(">>> Scheduled is running on another server...");
        return;
    }
    try {
        // doSomething();
    } finally {
        redisUtils.releaseLock(lockKey, "1");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

8 Github 项目

此演示项目已上传到 Github,如有需要可自行下载,欢迎 Star 。

https://github.com/LucioChn/spring-schedule


本次分享到此结束啦~~

如果觉得文章对你有帮助,点赞、收藏、关注、评论,您的支持就是我创作最大的动力!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/728200
推荐阅读
相关标签
  

闽ICP备14008679号