schedule

springboot 常见的两个开发场景:

1、rest 接口服务:前端接口服务,后台OpenApi接口服务

2、定时任务:定时数据集成,定时数据计算,定时扫描redis,发邮件短信定时告警等等。

Java线程框架非常复杂,从一般的悲观锁,乐观锁,AQS等等,设计了大量的类,容器,但是对于使用场景尤其非互联网项目,做CICD,自动化运维,自动化运营,OA类项目,大多情况不会使用原生的Timer,Schedule。

对于定时任务,分布式定时任务,以前常用Quarz框架,XXL-JOB,其实轻量级的直接使用@Schedule就够了。

1、第一个定时任务

package com.wht.spring.boot.schedule;

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

@Component
public class HelloSchedule {
    @Scheduled(fixedRate = 2000L)
    public void sayHello(){
        System.out.println(Thread.currentThread().getName()+"-sayHello!");
    }
}

查看console没有任何反应

  • 启动类必须添加 @EnableScheduling

    ​ springboot核心特性组件化和自动装配,为了精简并不是把所有模块都装配上,所以要开启定时任务功能,手工添加注解

  • @Component 调度必须是组件,才能自动装配进context

  • sayHello() 必须是无参方法 看下源码就明白了,org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.class#processScheduled

  • @Scheduled(fixedRate = 2000L) 必须,并且必须又一个表示频率的值也是看上述源码

@Scheduled的定时策略有如下几种方式:

  • cron 定时任务表达式:@Scheduled(cron = "* * * * * * ") 每秒
  • fixedRate: 定时多久执行一次(上一次开始执行时间点后xx秒再次执行;)
  • fixedDelay: 上一次执行结束时间点后xx秒再次执行

2、单个定时任务的深入测试

补充上@EnableScheduling之后就跑起来了

pool-1-thread-1-sayHello!
pool-1-thread-1-sayHello!
pool-1-thread-1-sayHello!

从打印的进程名称就能看出来,使用的pool思想

2.1、执行时间大于制定频率

@Component
public class HelloSchedule {
    public static final ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    @Scheduled(fixedRate = 1000L)
    public void sayHello() throws InterruptedException {
        System.out.println(df.get().format(new Date())+"\t"+Thread.currentThread().getName() + "-sayHello!");
        Thread.sleep(5000);
    }
}

2022-09-29 10:01:16	pool-1-thread-1-sayHello!
2022-09-29 10:01:21	pool-1-thread-1-sayHello!
2022-09-29 10:01:26	pool-1-thread-1-sayHello!
2022-09-29 10:01:31	pool-1-thread-1-sayHello!
2022-09-29 10:01:36	pool-1-thread-1-sayHello!
2022-09-29 10:01:41	pool-1-thread-1-sayHello!
2022-09-29 10:01:46	pool-1-thread-1-sayHello!
2022-09-29 10:01:51	pool-1-thread-1-sayHello!
2022-09-29 10:01:56	pool-1-thread-1-sayHello!

明显会阻塞,而非根据频率定时生成任务,放到线程池并行执行。

2.2、如果任务异常了调度会不会挂

@Component
public class HelloSchedule {
    public static final ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    @Scheduled(cron = "* * * * * * ")
    public void sayHello() throws InterruptedException {
        System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello!");
        Thread.sleep(5000);
        System.out.println(1 / 0);
    }
}


实验证明不会挂,打印异常之后继续跑

org.springframework.scheduling.support.ScheduledMethodRunnable.class#run()

这个方法把异常捕获处理了,不会导致程序中断。

2022-09-29 10:05:46	pool-1-thread-1-sayHello!
2022-09-29 10:05:51.013 ERROR 16684 --- [pool-1-thread-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task.

java.lang.ArithmeticException: / by zero
	at com.wht.spring.boot.schedule.HelloSchedule.sayHello(HelloSchedule.java:17) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_341]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_341]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_341]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_341]
	at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:93) [spring-context-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_341]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_341]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_341]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_341]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_341]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_341]
	at java.lang.Thread.run(Thread.java:750) [na:1.8.0_341]

3、多个任务之间是不是并行的

真正的项目一般调度包是独立的,往往代表项目的任务模块,自然不至于一个任务。

下面有三个任务会怎么执行

@Component
public class HelloSchedule {
    public static final ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    @Scheduled(cron = "* * * * * * ")
    public void sayHello() throws InterruptedException {
        System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello!");
        Thread.sleep(10000);
    }

    @Scheduled(cron = "* * * * * * ")
    public void sayHello1() throws InterruptedException {
        System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello1!");
        Thread.sleep(5000);
    }
    @Scheduled(cron = "* * * * * * ")
    public void sayHello2() throws InterruptedException {
        System.out.println(df.get().format(new Date()) + "\t" + Thread.currentThread().getName() + "-sayHello2!");
    }
}

看下执行结果:

2022-09-29 10:14:09	pool-1-thread-1-sayHello1!
2022-09-29 10:14:14	pool-1-thread-1-sayHello!
2022-09-29 10:14:24	pool-1-thread-1-sayHello2!
2022-09-29 10:14:24	pool-1-thread-1-sayHello1!

sayHello2 并不能疫苗执行一次,三个任务好像只有一个线程,串起来了。

这就是为什么有些新同事开发了调度,结果生产经常不跑原因之一。

org.springframework.scheduling.config.SchedulerBeanDefinitionParser.class#doParse
pool-size
org\springframework\scheduling\config\spring-task.xsd  中说明了default-value  1
        
        
        if (this.taskScheduler == null) {
            this.localExecutor = Executors.newSingleThreadScheduledExecutor();
            this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
        }

3.1、方案一自己提供执行器,替代掉原始的newSingleThreadScheduledExecutor

这有个好处,就是可以自定义很多信息,便于个性化

@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {

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

    @Bean(destroyMethod="shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(100);
    }
}

执行结果:

2022-09-29 10:32:28	pool-1-thread-1-sayHello!
2022-09-29 10:32:28	pool-1-thread-2-sayHello1!
2022-09-29 10:32:28	pool-1-thread-3-sayHello2!
2022-09-29 10:32:29	pool-1-thread-3-sayHello2!
2022-09-29 10:32:30	pool-1-thread-4-sayHello2!

可以看到线程有多个了,多个任务之间可以并行了

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

Spring Bootapplication.properties 中相关的配置说明:

# 任务调度线程池

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


# 任务执行线程池配置

# 是否允许核心线程超时。这样可以动态增加和缩小线程池
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=
#  线程池 队列容量大小
spring.task.execution.pool.queue-capacity=
# 线程池关闭时等待所有任务完成
spring.task.execution.shutdown.await-termination=true
# 执行线程关闭前最大等待时间,确保最后一定关闭
spring.task.execution.shutdown.await-termination-period=
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-

以上配置已经很全了,但是还是单线程阻塞,springboot升级到

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

实验了才知道为什么

posted @ 2022-09-29 12:33  红尘过客2022  阅读(421)  评论(0编辑  收藏  举报