参数详解
- @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 表达式
语法格式:
- 秒 分 小时 月份中的日期 月份 星期中的日期 年份
- 秒 分 小时 月份中的日期 月份 星期中的日期
字段值特别字符秒(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, - * /
- *:表示匹配该域的任意值。
例如:在 Minutes 域使用*,即表示每分钟都会触发事件
- ?:只能用在 DayofMonth 和 DayofWeek 两个域,它也匹配域的任意值,但实际不会,因为 DayofMonth 和 DayofWeek 会相互影响。
例如:在每月的 20 日触发使命,不管 20 日是星期几,只能使用如下写法:13 13 15 20 * ?,此中最后一位只能用?,而不能使用,假如使用表示不管星期几都会触发
- -:表示范围。
例如:在 Minutes 域使用 5-20,表示从 5 到 20 分钟每分钟触发一次
- /:表示起始时间开始触发,然后每隔固定时间触发一次。
例如:在 Minutes 域使用 5/20,则意味着从第 5 分钟开始,每隔 20 分钟触发一次
- ,:表示列出枚举值。
例如:在 Minutes 域使用 5,20,则意味着在 5 和 20 分都会触发一次
- L:表示最后,只能出如今 DayofWeek 和 DayofMonth 域。
例如:在 DayofWeek 域使用 5L,意味着在最后的一个星期四触发
- W:表示有效工作日(周一到周五),只能出如今 DayofMonth 域,体系将在离指定日期的最近的有效工作日触发事件。
例如:在 DayofMonth 使用 5W,假如 5 日是星期六,则将在最近的工作日(星期五,即 4 日触发);假如 5 日是星期天,则在 6 日(星期一)触发;假如 5 日在星期一到星期五中的一天,则就在 5 日触发。注意:W 的最近寻找不会跨过月份
- LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五
- #:用于确定每个月第 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: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触发
复制代码 基本使用
基本方法
- <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");
- }
- }
复制代码 定时使命开/关
通过设置文件控制Bean的实例化,根据需要进行开启/关闭定时使命- @Component
- @Slf4j
- @RefreshScope
- @ConditionalOnProperty(prefix = "test.job", name = "enable", havingValue = "true", matchIfMissing = true)
- public class TestJob {
-
- @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 使命执行器线程池,主要用于执行单次使命
自动设置条件
- 当类路径下存在 ThreadPoolTaskExecutor 类
- 当 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
- @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 默认值:
- 线程名称前缀:threadNamePrefix = “task-”
- 核心线程数:coreSize = 8
- 最大线程数:maxSize = Integer.MAX_VALUE
- 非核心线程存活时长:keepAlive = Duration.ofSeconds(60)
调度器
SpringBoot 内默认自动设置 TaskScheduler 使命调度器线程池,主要用于执行周期性使命
自动设置条件
- 当类路径下存在 ThreadPoolTaskScheduler 类
- 当 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
- @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;
- }
- }
复制代码
- 当 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 默认设置值:
- 线程名称前缀:threadNamePrefix = “scheduling-”
- 线程数: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);
- }
- }
复制代码 @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());
- }
- }
复制代码 通过指定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 {
- @Override
- public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
- taskRegistrar.setScheduler(taskExecutor());
- }
- @Bean
- public 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 {
-
- @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 设置
修改默认的线程池设置,适当将调度线程池的设置修改,支持多使命并发执行- ######任务调度线程池######
- # 任务调度线程池大小 默认 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-
复制代码 缺点
- 不支持集群设置,在分布式环境下会出现多个使命并发执行的情况
解决方法:通过分布式锁的方式预防使命并发执行的情况
- 不支持指定的时间范围执利用命(例如在9点到11点间执利用命,其他时间段不执行)
- 不支持分片执利用命
动态定时使命实现
出现问题
用实现 SpringBoot + @Scheduled 实现了定时使命。但是也存在很多问题:
通常,@Scheduled 注解的所有属性只在 Spring Context 启动时剖析和初始化一次。因此,当在 Spring 中使用 @Scheduled 注解时,无法在运行时更改 fixedDelay 或 fixedRate 值。
- 在一个线程内执行,那么使命多了就可能被阻塞,导致使命耽误执行。
- 每次修改执行频率都要改代码,重启服务。
- 无法提供定时使命的启用、暂停、修改接口。
实现方法:参考 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 {
- @Bean
- public 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<>();
- @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);
- }
- }
复制代码- @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";
- }
- }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |