SpringBoot Task

相关文章链接

  1. 定时任务工具类(Cron Util)
  2. SpringBoot Task

参数详解

java 复制代码
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
    String 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 "";
}

fixedDelay

它的间隔时间是根据上次任务结束的时候开始计时的,只要盯紧上一次任务执行结束的时间即可,跟任务逻辑的执行时间无关,两个任务的间隔时间是固定的

fixedDelayString

与 fixedDalay 一样,不同的是使用的是 String 字符串,支持占位符方式

java 复制代码
@Scheduled(fixedDelayString = "${time.fixedDelay}")
public void test() {
    System.out.println("Execute at " + System.currentTimeMillis());
}

fixedRate

在理想情况下,下一次开始和上一次开始之间的时间间隔是一定的,但是默认情况下 SpringBoot 定时任务是单线程执行的。当下一轮的任务满足时间策略后任务就会加入队列,即当本次任务开始执行时下一次任务的时间就已经确定了,由于本次任务的"超时"执行,下一次任务的等待时间就会被压缩甚至阻塞

fixedRateString

与 fixedRate 一样,不同的是使用的是 String 字符串,支持占位符方式

initialDelay

这个参数只能配合 fixedDelay 或 fixedRate 使用。如:@Scheduled(initialDelay = 10000, fixedRate = 15000),意思是在容器启动后,延迟 10 秒再执行一次定时器,以后每 15 秒再执行一次该定时器

initialDelayString

与 initialDelay 一样,不同的是使用的是 String 字符串,支持占位符方式

cron 表达式

语法格式:

  1. 秒 分 小时 月份中的日期 月份 星期中的日期 年份
  2. 秒 分 小时 月份中的日期 月份 星期中的日期
字段 特殊字符
秒(Seconds) 0~59 的整数 , - * /
分(Minutes) 0~59 的整数 , - * /
小时(Hours) 0~23 的整数 , - * /
日期(DayofMonth) 1~31 的整数(需要看月的天数) , - * ? / L W C
月份(Month) 1~12 的整数 , - * /
星期(DayOfWeek) 1~7 的整数 , - * ? / L W C
年(Year)(可选) 1970~2099 , - * /
  1. *:表示匹配该域的任意值。

    例如:在 Minutes 域使用*,即表示每分钟都会触发事件

  2. ?:只能用在 DayofMonth 和 DayofWeek 两个域,它也匹配域的任意值,但实际不会,因为 DayofMonth 和 DayofWeek 会相互影响。

    例如:在每月的 20 日触发任务,不管 20 日是星期几,只能使用如下写法:13 13 15 20 * ?,其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发

  3. -:表示范围。

    例如:在 Minutes 域使用 5-20,表示从 5 到 20 分钟每分钟触发一次

  4. /:表示起始时间开始触发,然后每隔固定时间触发一次。

    例如:在 Minutes 域使用 5/20,则意味着从第 5 分钟开始,每隔 20 分钟触发一次

  5. ,:表示列出枚举值。

    例如:在 Minutes 域使用 5,20,则意味着在 5 和 20 分都会触发一次

  6. L:表示最后,只能出现在 DayofWeek 和 DayofMonth 域。

    例如:在 DayofWeek 域使用 5L,意味着在最后的一个星期四触发

  7. W:表示有效工作日(周一到周五),只能出现在 DayofMonth 域,系统将在离指定日期的最近的有效工作日触发事件。

    例如:在 DayofMonth 使用 5W,如果 5 日是星期六,则将在最近的工作日(星期五,即 4 日触发);如果 5 日是星期天,则在 6 日(星期一)触发;如果 5 日在星期一到星期五中的一天,则就在 5 日触发。注意:W 的最近寻找不会跨过月份

  8. LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五

  9. #:用于确定每个月第 n 个星期 x(x#n),只能出现在 DayofMonth 域。

    例如:4#2 表示第 2 个星期三

常用表达式参考

java 复制代码
"*/5 * * * * ?"       # 每隔5秒执行一次
"0 */1 * * * ?"       # 每隔1分钟执行一次
"0 0 23 * * ?"        # 每天23点执行一次
"0 0 1 * * ?"         # 每天凌晨1点执行一次
"0 0 1 1 * ?"         # 每月1号凌晨1点执行一次
"0 0 23 L * ?"        # 每月最后一天23点执行一次
"0 0 1 ? * L"         # 每周星期天凌晨1点实行一次:
"0 26,29,33 * * * ?"  # 在26分、29分、33分执行一次
"0 0 0,3,8,21 * * ?"  # 每天的0点、3点、8点、21点执行一次
"0 0 10,14,16 * * ?"  # 每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?"   # 朝九晚五工作时间内每半小时
"0 0 12 ? * WED"      # 表示每个星期三中午12点
"0 0 12 * * ?"        # 每天中午12点触发
"0 15 10 ? * *"       # 每天上午10:15触发
"0 15 10 * * ?"       # 每天上午10:15触发
"0 15 10 * * ? *"     # 每天上午10:15触发
"0 15 10 * * ?"       # 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?"        # 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?"      # 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?"   # 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?"      # 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED"  # 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" # 周一至周五的上午10:15触发
"0 15 10 15 * ?"      # 每月15日上午10:15触发
"0 15 10 L * ?"       # 每月最后一日的上午10:15触发
"0 15 10 ? * 6L"      # 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3"     # 每月的第三个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" # 2002年至2005年的每月的最后一个星期五上午10:15触发

基本使用

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
	<version>2.3.12.RELEASE</version>
</dependency>
java 复制代码
@SpringBootApplication
// 开启定时任务开关
@EnableScheduling
public class SpringtaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringtaskApplication.class, args);
    }
}
java 复制代码
@Component
public class TaskService01 {
    
    @Scheduled(fixedDelay = 1000)
    public void task01(){
        System.out.println("fixedDelay....");
    }
    
    @Scheduled(fixedRate = 1000)
    public void task02(){
        System.out.println("fixedRate....");
    }
    
    @Scheduled(initialDelay = 10000,fixedDelay = 1000)
    public void task03(){
        System.out.println("initialDelay");
    }
    
    @Scheduled(cron = "1 * * * * *")
    public void task04(){
        System.out.println("cron");
    }
}

定时任务配置

@EnableScheduling 注解引入了 ScheduledAnnotationBeanPostProcessor 其 setScheduler(Object scheduler) 有以下的注释:

如果 TaskScheduler 或者 ScheduledExecutorService 没有定义为该方法的参数,该方法将在 Spring IoC 中寻找唯一的 TaskScheduler 或者名称为 taskScheduler 的 Bean 作为参数,当然你按照查找 TaskScheduler 的方法找一个 ScheduledExecutorService 也可以。要是都找不到那么只能使用本地单线程调度器了

执行器

SpringBoot 内默认自动配置 TaskExecutor 任务执行器线程池,主要用于执行单次任务

自动配置条件

  1. 当类路径下存在 ThreadPoolTaskExecutor 类
  2. 当 Spring 容器中不存在 Executor 的 bean
java 复制代码
// 仅在类 ThreadPoolTaskExecutor 存在于 classpath 时才应用
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TaskExecutionProperties.class)
public class TaskExecutionAutoConfiguration {

    public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";

    @Bean
    @ConditionalOnMissingBean
    public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
            ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
            ObjectProvider<TaskDecorator> taskDecorator) {
        TaskExecutionProperties.Pool pool = properties.getPool();
        TaskExecutorBuilder builder = new TaskExecutorBuilder();
        builder = builder.queueCapacity(pool.getQueueCapacity());
        builder = builder.corePoolSize(pool.getCoreSize());
        builder = builder.maxPoolSize(pool.getMaxSize());
        builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
        builder = builder.keepAlive(pool.getKeepAlive());
        Shutdown shutdown = properties.getShutdown();
        builder = builder.awaitTermination(shutdown.isAwaitTermination());
        builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
        builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
        builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator);
        builder = builder.taskDecorator(taskDecorator.getIfUnique());
        return builder;
    }

    @Lazy
    @Bean(name = { APPLICATION_TASK_EXECUTOR_BEAN_NAME,
            AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
    @ConditionalOnMissingBean(Executor.class)
    public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
        return builder.build();
    }
}

线程池配置

TaskExecutionProperties 默认值:

  1. 线程名称前缀:threadNamePrefix = "task-"
  2. 核心线程数:coreSize = 8
  3. 最大线程数:maxSize = Integer.MAX_VALUE
  4. 非核心线程存活时长:keepAlive = Duration.ofSeconds(60)

调度器

SpringBoot 内默认自动配置 TaskScheduler 任务调度器线程池,主要用于执行周期性任务

自动配置条件

  1. 当类路径下存在 ThreadPoolTaskScheduler 类
  2. 当 Spring 容器中不存在 SchedulingConfigurer 、 TaskScheduler 、ScheduledExecutorService 的 bean
java 复制代码
@ConditionalOnClass(ThreadPoolTaskScheduler.class)
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TaskSchedulingProperties.class)
@AutoConfigureAfter(TaskExecutionAutoConfiguration.class)
public class TaskSchedulingAutoConfiguration {

    @Bean
    @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
    public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
        return builder.build();
    }

    @Bean
    @ConditionalOnMissingBean
    public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties,
            ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
        TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
        builder = builder.poolSize(properties.getPool().getSize());
        Shutdown shutdown = properties.getShutdown();
        builder = builder.awaitTermination(shutdown.isAwaitTermination());
        builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
        builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
        builder = builder.customizers(taskSchedulerCustomizers);
        return builder;
    }
}
  1. 当 Spring 容器中存在名字叫 org.springframework.context.annotation.internalScheduledAnnotationProcessor (需要配置 @EnableScheduling 注解将会注入这个名字的 bean)
java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}
java 复制代码
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

    @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

线程池配置

TaskSchedulingProperties 默认配置值:

  1. 线程名称前缀:threadNamePrefix = "scheduling-"
  2. 线程数:size = 1

该配置的自定义配置以 spring.task.scheduling 开头。同时它需要在任务执行器配置 TaskExecutionAutoConfiguration 配置后才生效。我们只需要在中对其配置属性 spring.task.execution 相关属性配置即可。

注意:定义任务默认用的是 TaskSchedulingAutoConfiguration 实例化的 Bean(applicationTaskExecutor、taskScheduler)

Properties 配置

properties 复制代码
######任务调度线程池######
# 任务调度线程池大小 默认 1 建议根据任务加大
spring.task.scheduling.pool.size=1
# 调度线程名称前缀 默认 scheduling-
spring.task.scheduling.thread-name-prefix=scheduling-
# 线程池关闭时等待所有任务完成
spring.task.scheduling.shutdown.await-termination=true
# 调度线程关闭前最大等待时间,确保最后一定关闭
spring.task.scheduling.shutdown.await-termination-period=60


######任务执行线程池配置######
# 是否允许核心线程超时。这样可以动态增加和缩小线程池
spring.task.execution.pool.allow-core-thread-timeout=true
#  核心线程池大小 默认 8
spring.task.execution.pool.core-size=8
# 线程空闲等待时间 默认 60s
spring.task.execution.pool.keep-alive=60s
# 线程池最大数  根据任务定制
spring.task.execution.pool.max-size=16
#  线程池 队列容量大小
spring.task.execution.pool.queue-capacity=10
# 线程池关闭时等待所有任务完成
spring.task.execution.shutdown.await-termination=true
# 执行线程关闭前最大等待时间,确保最后一定关闭
spring.task.execution.shutdown.await-termination-period=60
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-

TaskSchedulingAutoConfiguration 源码

当 Spring Boot 应用程序中没有定义自定义的线程池 bean 时,Spring Boot 应用程序会根据自动配置类注入一个名为 applicationTaskExecutor 或 taskExecutor 的线程池对象,它的配置是在 TaskExecutionProperties 类中完成的,这个类使用 spring.task.execution 前缀进行配置,包含了很多线程池相关细节的配置选项,当我们容器中存在自定义线程池时,applicationTaskExecutor 或 taskExecutor 的线程池对象是不会被创建的。

@Async 注解相关配置

使用@Async 注解没有指定 value 属性时,项目启动的时候会有这样的提示:"在上下文中找到多个 TaskExecutor bean,并且没有一个名为' taskExecutor'。将其中一个标记为 primary 或将其命名为'taskExecutor'(可能作为别名),以便将其用于异步处理"

java 复制代码
// 标记为 Primary,即主要的线程
@Bean
@Primary
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setThreadNamePrefix("my-free-style-");
    executor.setMaxPoolSize(maxPoolSize);
    executor.setCorePoolSize(corePoolSize);
    executor.setQueueCapacity(queueCapacity);
    executor.setKeepAliveSeconds(keepAliveSeconds);
    // 线程池对拒绝任务(无线程可用)的处理策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
}
 
// 直接起别名为 taskExecutor
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setThreadNamePrefix("my-free-style-");
    executor.setMaxPoolSize(maxPoolSize);
    executor.setCorePoolSize(corePoolSize);
    executor.setQueueCapacity(queueCapacity);
    executor.setKeepAliveSeconds(keepAliveSeconds);
    // 线程池对拒绝任务(无线程可用)的处理策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
}

任务阻塞

出现原因

Spring 中@EnableScheduling 和@Scheduled 标注的定时任务默认单线程同步执行,多个任务时,一个任务执行完毕以后才能执行下一个任务,可能会有阻塞现象发生(如果希望并发运行,需要配置线程池)

java 复制代码
@SpringBootApplication
@EnableScheduling
public class SpringbootTaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootTaskApplication.class, args);
    }
}
java 复制代码
@Component
@Slf4j
public class ScheduleTask1 {

    @Scheduled(cron = "*/2 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("我是task1,我需要执行 10s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(10000);
        log.info("我是task1 ending ,我的线程的 id == > {} , 时间 == > {}", Thread.currentThread().getId(), new Date());
    }

    @Scheduled(cron = "*/4 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("我是task2,我需要执行 2s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(2000);
        log.info("我是task2 ending ,我的线程的 id == > {} , 时间 == > {}",Thread.currentThread().getId(), new Date());
    }
}
java 复制代码
// 运行结果
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 95,时间 == >Fri Feb 01 15:16:52 CST 2019
我是task1 ending ,我的线程的 id == > 95 , 时间 == > Fri Feb 01 15:17:02 CST 2019
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 95,时间 == >Fri Feb 01 15:17:02 CST 2019
task2 ending ,我的线程的 id == > 95 , 时间 == > Fri Feb 01 15:17:04 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 95,时间 == >Fri Feb 01 15:17:04 CST 2019
task1 ending ,我的线程的 id == > 95 , 时间 == > Fri Feb 01 15:17:14 CST 2019

可以看出,从 task1 任务运行时,等到 4s 时,task2 任务没有执行,而是等到 task1 任务执行结束后才执行

解决方法

使用@Async 异步执行任务

java 复制代码
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class SpringbootTaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootTaskApplication.class, args);
    }
}
  1. 使用默认线程池配置

@Async默认的线程池配置是Bean名称为taskExecutor的类

java 复制代码
@Component
@Slf4j
public class ScheduleTask2 {

    @Async
    @Scheduled(cron = "*/2 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("我是task1,我需要执行 10s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(10000);
        log.info("我是task1 ending ,我的线程的 id == > {} , 时间 == > {}", Thread.currentThread().getId(), new Date());
    }

    @Async
    @Scheduled(cron = "*/4 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("我是task2,我需要执行 2s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(2000);
        log.info("我是task2 ending ,我的线程的 id == > {} , 时间 == > {}",Thread.currentThread().getId(), new Date());
    }
}
  1. 自定义线程池配置

通过指定Bean名称来决定使用哪个线程池,用户可以自定义线程池配置

java 复制代码
@Component
@Slf4j
public class ScheduleTask3 {

    @Async("myPoolTaskExecutor")
    @Scheduled(cron = "*/2 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("我是task1,我需要执行 10s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(10000);
        log.info("我是task1 ending ,我的线程的 id == > {} , 时间 == > {}", Thread.currentThread().getId(), new Date());
    }

    @Async("myPoolTaskExecutor")
    @Scheduled(cron = "*/4 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("我是task2,我需要执行 2s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(2000);
        log.info("我是task2 ending ,我的线程的 id == > {} , 时间 == > {}",Thread.currentThread().getId(), new Date());
    }

    /**
    * 创建自定义线程池,提供异步调用时使用
    **/
    @Bean(name = "myPoolTaskExecutor")
    public ThreadPoolTaskExecutor getMyPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //核心线程数
        taskExecutor.setCorePoolSize(10);
        //线程池维护线程的最大数量, 只有在缓冲队列满了之后才会申请超过核心线程数的线程
        taskExecutor.setMaxPoolSize(100);
        //缓存队列
        taskExecutor.setQueueCapacity(50);
        //许的空闲时间, 当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        taskExecutor.setKeepAliveSeconds(200);
        //异步方法内部线程名称
        taskExecutor.setThreadNamePrefix("poolTestThread-");
        /**
         * 当线程池的任务缓存队列已满并且线程池中的线程数目达到 maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
         * 通常有以下四种策略:
         * ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出 RejectedExecutionException 异常。
         * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
         * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         * ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
         */
        // 拒绝策略
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();

        System.out.println("@Async 业务处理线程配置成功,核心线程池:[{}],最大线程池:[{}],队列容量:[{}],线程名称前缀:[{}]");
        return taskExecutor;
    }
}

执行结果

java 复制代码
//运行结果:
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 116,时间 == >Fri Feb 01 16:19:32 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 117,时间 == >Fri Feb 01 16:19:32 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 124,时间 == >Fri Feb 01 16:19:34 CST 2019
task2 ending ,我的线程的 id == > 116 , 时间 == > Fri Feb 01 16:19:34 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 125,时间 == >Fri Feb 01 16:19:36 CST 2019
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 126,时间 == >Fri Feb 01 16:19:36 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 127,时间 == >Fri Feb 01 16:19:38 CST 2019
task2 ending ,我的线程的 id == > 126 , 时间 == > Fri Feb 01 16:19:38 CST 2019
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 128,时间 == >Fri Feb 01 16:19:40 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 129,时间 == >Fri Feb 01 16:19:40 CST 2019

从日志可知:task1 和 task2 的确是并行执行的,因为开始的时间节点是一样的。

存在问题:当 task1 第一次任务执行时间过长时,此时 task1 又到了其第二次执行任务的调度时间,这时会并行执行两个任务

实现 SchedulingConfigurer 接口

使用@Async 会导致第一次任务执行时间过长,从而第二次任务和第一次任务并发执行

解决方法:实现 SchedulingConfigurer 接口,这样自动装配中 TaskSchedulingAutoConfiguration 的 taskScheduler 就不会被实例化,替换原来的线程池配置

java 复制代码
@Configuration
@Slf4j
public class ScheduleConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }

     @Bean
     public Executor taskExecutor(){
         return Executors.newScheduledThreadPool(10);
     }
}
java 复制代码
@Component
@Slf4j
public class ScheduleTask4 {

    @Scheduled(cron = "*/2 * * * * ?")
    public void task1() throws InterruptedException {
        log.info("我是task1,我需要执行 10s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(10000);
        log.info("我是task1 ending ,我的线程的 id == > {} , 时间 == > {}", Thread.currentThread().getId(), new Date());
    }

    @Scheduled(cron = "*/4 * * * * ?")
    public void task2() throws InterruptedException {
        log.info("我是task2,我需要执行 2s 钟的时间,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(2000);
        log.info("我是task2 ending ,我的线程的 id == > {} , 时间 == > {}",Thread.currentThread().getId(), new Date());
    }
}

执行结果

java 复制代码
//执行结果:
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 95,时间 == >Fri Feb 01 16:28:16 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 96,时间 == >Fri Feb 01 16:28:16 CST 2019
task2 ending ,我的线程的 id == > 95 , 时间 == > Fri Feb 01 16:28:18 CST 2019
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 95,时间 == >Fri Feb 01 16:28:20 CST 2019
task2 ending ,我的线程的 id == > 95 , 时间 == > Fri Feb 01 16:28:22 CST 2019
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 121,时间 == >Fri Feb 01 16:28:24 CST 2019
task1 ending ,我的线程的 id == > 96 , 时间 == > Fri Feb 01 16:28:26 CST 2019
task2 ending ,我的线程的 id == > 121 , 时间 == > Fri Feb 01 16:28:26 CST 2019
我是task1,我需要执行 10s 钟的时间,我的线程的 id == > 95,时间 == >Fri Feb 01 16:28:28 CST 2019
我是task2,我需要执行 2s 钟的时间,我的线程的 id == > 122,时间 == >Fri Feb 01 16:28:28 CST 2019

注意:此时每次定时任务执行的 traceId 是一致的,无法很好地追踪每次定时任务的情况,修改如下

java 复制代码
@Configuration
@Slf4j
public class ScheduleConfig implements SchedulingConfigurer {
    
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // taskRegistrar.setScheduler(taskExecutor());  
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
		taskScheduler.setPoolSize(10);
		taskScheduler.initialize();
        taskRegistrar.setScheduler(taskScheduler);
    }

    // 缺点:可能每次定时任务产生的 traceId 是一致的
    // @Bean
    // public Executor taskExecutor(){
    //     return Executors.newScheduledThreadPool(10);
    // }
}

Properties 配置

修改默认的线程池配置,适当将调度线程池的配置修改,支持多任务并发执行

properties 复制代码
######任务调度线程池######
# 任务调度线程池大小 默认 1 建议根据任务加大
spring.task.scheduling.pool.size=10
# 调度线程名称前缀 默认 scheduling-
spring.task.scheduling.thread-name-prefix=scheduling-
# 线程池关闭时等待所有任务完成
spring.task.scheduling.shutdown.await-termination=true
# 调度线程关闭前最大等待时间,确保最后一定关闭
spring.task.scheduling.shutdown.await-termination-period=60


######任务执行线程池配置######
# 是否允许核心线程超时。这样可以动态增加和缩小线程池
spring.task.execution.pool.allow-core-thread-timeout=true
# 核心线程池大小 默认 8
spring.task.execution.pool.core-size=8
# 线程空闲等待时间 默认 60s
spring.task.execution.pool.keep-alive=60s
# 线程池最大数 根据任务定制
spring.task.execution.pool.max-size=16
# 线程池队列容量大小
spring.task.execution.pool.queue-capacity=10
# 线程池关闭时等待所有任务完成
spring.task.execution.shutdown.await-termination=true
# 执行线程关闭前最大等待时间,确保最后一定关闭
spring.task.execution.shutdown.await-termination-period=60
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-

缺点

  1. 不支持集群配置,在分布式环境下会出现多个任务并发执行的情况

解决方法:通过分布式锁的方式预防任务并发执行的情况

  1. 不支持指定的时间范围执行任务(例如在9点到11点间执行任务,其他时间段不执行)
  2. 不支持分片执行任务

动态定时任务实现

出现问题

用实现 SpringBoot + @Scheduled 实现了定时任务。但是也存在很多问题:

通常,@Scheduled 注解的所有属性只在 Spring Context 启动时解析和初始化一次。因此,当在 Spring 中使用 @Scheduled 注解时,无法在运行时更改 fixedDelay 或 fixedRate 值。

  1. 在一个线程内执行,那么任务多了就可能被阻塞,导致任务延迟执行。
  2. 每次修改执行频率都要改代码,重启服务。
  3. 无法提供定时任务的启用、暂停、修改接口。

实现方法:参考 ScheduledTaskRegistrar 源码提供的方法

简单案例

mysql 复制代码
CREATE TABLE `sys_task` (
  `id` bigint(21) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `task_uuid` varchar(50) DEFAULT NULL COMMENT '任务UUID',
  `task_name` varchar(50) DEFAULT NULL COMMENT '任务名称',
  `task_cron` varchar(50) DEFAULT NULL COMMENT '任务定时表达式',
  `class_name` varchar(100) DEFAULT NULL COMMENT '任务类',
  `method_name` varchar(100) DEFAULT NULL COMMENT '任务方法',
  `task_type` int(1) DEFAULT NULL COMMENT '任务类型',
  `remark` varchar(250) DEFAULT NULL,
  `del_flag` int(1) DEFAULT '1',
  `create_user` varchar(50) DEFAULT NULL,
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `update_user` varchar(50) DEFAULT NULL,
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
java 复制代码
@Configuration
public class ScheduledConfig {

    @Bean
    public ScheduledTaskRegistrar taskRegistrar() {
        return new ScheduledTaskRegistrar();
    }
}
java 复制代码
@Slf4j
public class ScheduleTask5 {

    public void task1() throws InterruptedException {
        log.info("我是task1,我的线程的 id == > {},时间 == >{}", Thread.currentThread().getId(), new Date());
        Thread.sleep(4000);
        log.info("我是task1 ending ,我的线程的 id == > {} , 时间 == > {}", Thread.currentThread().getId(), new Date());
    }
}
java 复制代码
@Data
public class SysTask {
    /**
    * 主键
    */
    private Long id;

    /**
    * 任务 UUID
    */
    private String taskUuid;

    /**
    * 任务名称
    */
    private String taskName;

    /**
    * 任务定时表达式
    */
    private String taskCron;

    /**
    * 任务类
    */
    private String className;

    /**
    * 任务方法
    */
    private String methodName;

    /**
    * 任务类型
    */
    private Integer taskType;

    /**
     * 备注
     */
    private String remark;

    /**
     * 删除标识
     */
    private Integer delFlag;

    /**
     * 创建人
     */
    private String createUser;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 修改人
     */
    private String updateUser;

    /**
     * 修改时间
     */
    private Date updateTime;
}
java 复制代码
@Service
@Slf4j
public class CronServiceImpl implements CronService {

    private static Map<String, ScheduledTask> scheduledTaskMap = new HashMap<>();
    @Resource
    private ScheduledTaskRegistrar taskRegistrar;

    @Override
    public void add(SysTask sysTask) {
        CronTask cronTask = new CronTask(getRunnable(sysTask), sysTask.getTaskCron());
        ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask);
        String uuid = UUID.randomUUID().toString();
        scheduledTaskMap.put(uuid, scheduledTask);
        log.info("添加任务成功, uuid == > {}, 任务名称 == > {}, 任务表达式 == > {}", uuid, sysTask.getTaskName(), sysTask.getTaskCron());
    }

    private Runnable getRunnable(SysTask sysTask) {
        return () -> {
            try {
                Class<?> aClass = Class.forName(sysTask.getClassName());
                Constructor<?> constructor = aClass.getConstructor();
                Object o = constructor.newInstance();
                Method method = aClass.getMethod(sysTask.getMethodName());
                method.invoke(o);
            } catch (Exception e) {
                e.printStackTrace();
            }
        };
    }

    @Override
    public void delete(String uuid) {
        try {
            ScheduledTask scheduledTask = scheduledTaskMap.get(uuid);
            scheduledTask.cancel();
            scheduledTaskMap.remove(uuid);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void update(SysTask sysTask) {
        this.delete(sysTask.getTaskUuid());
        this.add(sysTask);
    }
}
java 复制代码
@RestController
@RequestMapping("/cron")
public class CronController {

    @Resource
    private CronService cronService;

    @PostMapping("/add")
    public String add(@RequestBody SysTask sysTask) {
        cronService.add(sysTask);
        return "success";
    }

    @PostMapping("/delete")
    public String delete(String uuid) {
        cronService.delete(uuid);
        return "success";
    }

    @PostMapping("/update")
    public String update(@RequestBody SysTask sysTask) {
        cronService.update(sysTask);
        return "success";
    }
}
相关推荐
搞不懂语言的程序员1 分钟前
模板方法模式详解
java·开发语言·模板方法模式
〆、风神3 分钟前
Spring Boot 自定义 Redis Starter 开发指南(附动态 TTL 实现)
spring boot·redis·后端
Asthenia04129 分钟前
HashMap 扩容机制与 Rehash 细节分析
后端
DataFunTalk11 分钟前
不是劝退,但“BI”基础不佳就先“别搞”ChatBI了!
前端·后端
星星电灯猴12 分钟前
flutter项目 发布Google Play
后端
Java小混子16 分钟前
Spring MVC
java·spring·mvc
用户97044387811623 分钟前
按图搜索1688商品(拍立淘)API 返回值说明
javascript·后端·算法
Fly_hao.belief24 分钟前
Spring Boot 框架注解:@ConfigurationProperties
java·spring boot·后端
代码吐槽菌27 分钟前
基于SpringBoot的水产养殖系统【附源码】
java·数据库·spring boot·后端·毕业设计
尽一份心出一份力27 分钟前
等不是办法,干才有希望,快速跑通graphRag
后端·机器学习·开源