当前位置: 首页 > news >正文

SpringBoot Task

相关文章链接

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

参数详解

@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 字符串,支持占位符方式

@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 个星期三

常用表达式参考

"*/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:102: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触发

基本使用

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.12.RELEASE</version>
</dependency>
@SpringBootApplication
// 开启定时任务开关
@EnableScheduling
public class SpringtaskApplication {public static void main(String[] args) {SpringApplication.run(SpringtaskApplication.class, args);}
}
@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
// 仅在类 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@ConditionalOnMissingBeanpublic 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
@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@ConditionalOnMissingBeanpublic 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)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}
@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 配置

######任务调度线程池######
# 任务调度线程池大小 默认 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’(可能作为别名),以便将其用于异步处理”

// 标记为 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 标注的定时任务默认单线程同步执行,多个任务时,一个任务执行完毕以后才能执行下一个任务,可能会有阻塞现象发生(如果希望并发运行,需要配置线程池)

@SpringBootApplication
@EnableScheduling
public class SpringbootTaskApplication {public static void main(String[] args) {SpringApplication.run(SpringbootTaskApplication.class, args);}
}
@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());}
}
// 运行结果
我是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 异步执行任务

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

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

@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名称来决定使用哪个线程池,用户可以自定义线程池配置

@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;}
}

执行结果

//运行结果:
我是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 就不会被实例化,替换原来的线程池配置

@Configuration
@Slf4j
public class ScheduleConfig implements SchedulingConfigurer {@Overridepublic void configureTasks(ScheduledTaskRegistrar taskRegistrar) {taskRegistrar.setScheduler(taskExecutor());}@Beanpublic Executor taskExecutor(){return Executors.newScheduledThreadPool(10);}
}
@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());}
}

执行结果

//执行结果:
我是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 是一致的,无法很好地追踪每次定时任务的情况,修改如下

@Configuration
@Slf4j
public class ScheduleConfig implements SchedulingConfigurer {@Overridepublic 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 配置

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

######任务调度线程池######
# 任务调度线程池大小 默认 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 源码提供的方法

简单案例

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
@Configuration
public class ScheduledConfig {@Beanpublic ScheduledTaskRegistrar taskRegistrar() {return new ScheduledTaskRegistrar();}
}
@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());}
}
@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;
}
@Service
@Slf4j
public class CronServiceImpl implements CronService {private static Map<String, ScheduledTask> scheduledTaskMap = new HashMap<>();@Resourceprivate ScheduledTaskRegistrar taskRegistrar;@Overridepublic 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();}};}@Overridepublic void delete(String uuid) {try {ScheduledTask scheduledTask = scheduledTaskMap.get(uuid);scheduledTask.cancel();scheduledTaskMap.remove(uuid);} catch (Exception e) {e.printStackTrace();}}@Overridepublic void update(SysTask sysTask) {this.delete(sysTask.getTaskUuid());this.add(sysTask);}
}
@RestController
@RequestMapping("/cron")
public class CronController {@Resourceprivate 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";}
}

相关文章:

SpringBoot Task

相关文章链接 定时任务工具类&#xff08;Cron Util&#xff09;SpringBoot Task 参数详解 Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) Retention(RetentionPolicy.RUNTIME) Documented Repeatable(Schedules.class) public interface Scheduled {String C…...

学习路之压力测试--jmeter安装教程

Jmeter安装 0、先安装jdk:这里是安装jdk-8u211-windows-x64 1、百度网盘上下载 jdk和jmeter 链接: https://pan.baidu.com/s/1qqqaQdNj1ABT1PnH4hfeCw?pwdkwrr 提取码: kwrr 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 官网&#xff1a;Apache JMeter - D…...

大模型部署,运维,测试所需掌握的知识点

python环境部署: python3 -m site --user-base 返回用户级别的Python安装基础目录 sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1 将python3的默认路径/usr/bin/python3替…...

ECharts柱状图-带圆角的堆积柱状图,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个柱状图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供…...

java 并发编程 (2)Thread 类和 Runnable 接口详解

目录 1. Thread 类和 Runnable 接口的设计目的 1.1 为什么有 Thread 类和 Runnable 接口&#xff1f; 2. Thread 类实现的详细分析 2.1 Thread 类的构造方法 2.2 start() 方法的工作原理 2.3 run() 方法 2.4 join() 方法 3. Runnable 接口的实现和作用 3.1 Runnable 接…...

人工智能之数学基础:线性代数在人工智能中的地位

本文重点 从本文开始&#xff0c;我们将开启线性代数的学习&#xff0c;在线性代数中有向量、矩阵&#xff0c;以及各种性质&#xff0c;那么这些数学知识究竟和人工智能有什么关系呢&#xff1f; 重要性 机器学习和深度学习的本质就是训练模型&#xff0c;要想训练模型需要使…...

PostgreSQL WITH 子句:提高查询效率和可读性

PostgreSQL WITH 子句:提高查询效率和可读性 PostgreSQL 是一种功能强大的开源关系数据库管理系统,它以其稳定性、可靠性和高级功能而闻名。在 PostgreSQL 中,WITH 子句(也称为公用表表达式,CTE)是一种非常有用的特性,它允许用户在一个大的查询中创建一个临时的结果集,…...

TransFormer--解码器:前馈网络层、叠加和归一组件

TransFormer--解码器&#xff1a;前馈网络层、叠加和归一组件 解码器的下一个子层是前馈网络层&#xff0c;如下图所示。 解码器的前馈网络层的工作原理与我们在编码器中学到的完全相同 叠加和归一组件 和在编码器部分学到的一样&#xff0c;叠加和归一组件连接子层的输入和输…...

2024亚太杯国际赛C题参考文章50页+完整解题思路+数据处理+最终结果

中国宠物食品行业的发展趋势与汇率情景分析&#xff1a;基于多模型的量化预测与决策分析 一 、 摘要 本文针对宠物产业及相关产业的发展分析问题&#xff0c;采用多种数学建模方法和数据 分析技术&#xff0c;构建了一系列预测和评估模型。从宠物数量预测、全球市场分析、产业 …...

Kafka 分区分配及再平衡策略深度解析与消费者事务和数据积压的简单介绍

Kafka&#xff1a;分布式消息系统的核心原理与安装部署-CSDN博客 自定义 Kafka 脚本 kf-use.sh 的解析与功能与应用示例-CSDN博客 Kafka 生产者全面解析&#xff1a;从基础原理到高级实践-CSDN博客 Kafka 生产者优化与数据处理经验-CSDN博客 Kafka 工作流程解析&#xff1a…...

useEffect、useCallback、useMemo和memo的区别

前言 在构建现代 React 应用时&#xff0c;性能优化是一个关键考虑因素。随着组件的复杂性增加&#xff0c;合理管理状态和副作用变得尤为重要。React 提供了多个工具来帮助开发者优化组件性能&#xff0c;其中最常用的包括 useEffect、useCallback、useMemo 和 React.memo。这…...

layui树形组件点击树节点后高亮的解决方案

效果显示&#xff1a; 代码 //节点高亮var nodes document.getElementsByClassName("layui-tree-txt");for (var i 0; i < nodes.length; i) {if (nodes[i].innerHTML obj.data.title){nodes[i].style.color "#006BF9";nodes[i].style.fontWeight …...

大语言模型(LLM)安全:十大风险、影响和防御措施

一、什么是大语言模型&#xff08;LLM&#xff09;安全&#xff1f; 大语言模型&#xff08;LLM&#xff09;安全侧重于保护大型语言模型免受各种威胁&#xff0c;这些威胁可能会损害其功能、完整性和所处理的数据。这涉及实施措施来保护模型本身、它使用的数据以及支持它的基…...

02 —— Webpack 修改入口和出口

概念 | webpack 中文文档 | webpack中文文档 | webpack中文网 修改入口 webpack.config.js &#xff08;放在项目根目录下&#xff09; module.exports {//entry设置入口起点的文件路径entry: ./path/to/my/entry/file.js, }; 修改出口 webpack.config.js const path r…...

Go语言进阶依赖管理

1. Go语言进阶 1.1 Goroutine package mainimport ("fmt""time" )func hello(i int) {println("hello goroutine : " fmt.Sprint(i)) }func main() {for i : 0; i < 5; i {go func(j int) { hello(j) }(i) // 启动一个新的 goroutine&…...

集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1

蓝牙模组 - RF-BM-2340B1是基于美国TI的CC2340R5为核心设计的一款SimpleLink 2.4 GHz 无线模块。支持Bluetooth 5.3 Low Energy、Zigbee 、IEEE 802.15.4g、TI 15.4-Stack (2.4 GHz)及私有协议。集成了高性能ARM Cortex-M0处理器&#xff0c;具有512 KB Flash、32 KB超低泄漏SR…...

ffmpeg本地编译不容易发现的问题 — Error:xxxxx not found!

这里区分电脑CPU架构 本次编译是在Mac笔记本&#xff0c;M1芯片上进行&#xff01; 前面大致流程&#xff1a;分为两种&#xff08;1.仅适用&#xff0c;直接下载编译好的本地安装即可&#xff1b;2.使用并查看源码&#xff0c;自己修改编译运行&#xff09;。这里介绍的是第…...

mybatis——Mapper代理方式

一、原始DAO开发问题 Dao接口实现类方法中存在大量模板方法&#xff0c;设想能否将这些代码提取出来&#xff0c;大大减轻程序员的工作 量。 调用sqlSession的数据库操作方法需要指定statement的id&#xff0c;这里存在硬编码&#xff0c;不利于开发维护。 调用SqlSession方…...

FreeRTOS——消息队列

目录 一、概念及其作用 1.1概念 1.2特点 1.3工作原理 二、相关API 2.1创建队列 2.2任务中写队列 2.3任务中读队列 2.4中断中写队列 2.5中断中读队列 三、实现原理 3.1消息队列控制块 3.2消息队列的创建 3.3消息的发送 3.3.1任务中发送 3.3.2中断中发送 3.4消息的…...

【题解】—— LeetCode一周小结46

&#x1f31f;欢迎来到 我的博客 —— 探索技术的无限可能&#xff01; &#x1f31f;博客的简介&#xff08;文章目录&#xff09; 【题解】—— 每日一道题目栏 上接&#xff1a;【题解】—— LeetCode一周小结45 11.切棍子的最小成本 题目链接&#xff1a;1547. 切棍子的最…...

Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导 一、前言 在充满活力与激情的校园生活中&#xff0c;校运会不仅是…...

python里的数据结构

列表&#xff08;List&#xff09; 定义和特点&#xff1a; 列表是一种有序的可变序列&#xff0c;可包含不同类型的元素&#xff0c;如整数、字符串、列表等。可以通过索引访问和修改元素&#xff0c;索引从 0 开始。代码示例&#xff1a; my_list [1, 2, apple, [4, 5]] pr…...

[Unity Demo]从零开始制作空洞骑士Hollow Knight第二十一集:制作游戏的金钱系统吉欧Geo和初步制作HUD Canvas的额外内容

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、制作游戏的金钱系统吉欧Geo以及HUD Camera 1.制作金钱系统吉欧2.制作吉欧的脚本Geo Counter逻辑处理3.制作HUD Canvas的吉欧的UI4.在敌人的HealthManager.c…...

底层逻辑之:极大似然方法(Maximum Likelihood Estimation, MLE)

简介&#xff1a; 极大似然方法&#xff08;Maximum Likelihood Estimation, MLE&#xff09;是一种用于估计统计模型参数的方法。其核心思想是基于观测数据来寻找最可能产生这些数据的模型参数。 早在1821年&#xff0c;德国数学家高斯&#xff08;C. F. Gauss&#xff09;就…...

笔记:Centos Nginx Jdk Mysql OpenOffce KkFile Minio安装部署

远程工具 ToDesk Nginx 解压 tar zxvf nginx-1.20.2.tar.gz进入Nginx 文件夹 cd nginx-1.20.2报错解决 ./configure: error: C compiler cc is not found yum -y install gcc gcc-c autoconf automake make./configure: error: the HTTP rewrite module requires the PC…...

【MARL】深入理解多智能体近端策略优化(MAPPO)算法与调参

&#x1f4e2;本篇文章是博主强化学习&#xff08;RL&#xff09;领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅…...

深入探索Go语言中的sync.Mutex与sync.RWMutex:原理、应用与实践

深入探索Go语言中的sync.Mutex与sync.RWMutex:原理、应用与实践 在并发编程的世界里,Go语言以其独特的并发模型和简洁的语法赢得了广泛的关注。在Go语言的并发控制工具箱中,sync.Mutex和sync.RWMutex是两个至关重要的工具,它们帮助开发者保护共享资源,避免竞态条件,确保…...

15.postgresql--jsonb 数组进行打平,过滤

用jsonb_array_elements函数先展开数组&#xff0c;再用jsonb_each函数遍历元素中的键值对 例如&#xff1a; SELECT * FROM data_table, LATERAL jsonb_array_elements(json_column) WITH ORDINALITY as elem(element, idx) JOIN LATERAL jsonb_each(elem.element) as kv(ke…...

linux下i2c开发与框架源码分析

目录 1 概述 2 I2c子系统框架 3 I2C的使用流程 3.1 在驱动里使用 3.2 在应用层使用 3.3 I2ctool的使用 4 为硬件i2c注册一个适配器 5 i2c子系统源码流程分析 5.1 i2c device与driver绑定过程 5.1.1 Driver的注册与处理 5.1.2 Client device的生成 5.2 I2c的发送与接…...

[ruby on rails] 安装docker

1. docker安装 ubuntu14.04后自带docker安装包&#xff0c;可以直接安装docker.io sudo apt-get updatesudo apt-get install -y docker.io # 安装后启动sudo service docker start最新版本docker 安装docker-ce # 官方源 curl -fsSL https://download.docker.com/linux/ubun…...