Files
rikako-note/spring/spring boot/Spring Boot Async.md
2023-01-10 21:59:13 +08:00

12 KiB
Raw Blame History

Spring Boot Async

Spring Executor和Scheduler的自动配置

当当前上下文中没有Executor类型的bean对象时spring boot会自动配置一个ThreadPoolTaskExecutor类型的bean对象并且将该bean对象和异步task执行@EnableAsync和spring mvc异步请求处理关联在一起。
该默认创建的ThreadPoolTaskExecutor默认使用8个核心线程并且线程数量可以根据负载动态的增加或者减少。
可以通过如下方式对ThreadPoolTaskExecutor进行配置

# 该线程池最多含有16个线程
spring.task.execution.pool.max-size=16
# 有有界队列存放task上限为100
spring.task.execution.pool.queue-capacity=100
# 当线程空闲10s默认60s时会进行回收
spring.task.execution.pool.keep-alive=10s
# 设置核心线程数量
spring.task.execution.pool.core-size=8

如果使用@EnableScheduling一个ThreadPoolTaskScheduler也可以被配置。该线程池默认使用一个线程但是也可以动态设置

spring.task.scheduling.thread-name-prefix=scheduling-
spring.task.scheduling.pool.size=2

Task Execution and Scheduling

Spring通过TaskExecutor和TaskScheduler接口为task的异步执行和调度提供了抽象。

Task Execution Abstraction

Executor对应的是JDK中线程池的概念。在Spring中TaskExecutor接口和java.util.concurrent.Executor接口相同该接口中只有一个execute方法execute(Runnable task))接受一个task。

TaskExecutor接口的实现种类

Spring中包含许多预制的TaskExecutor实现类该实现类如下

  • SyncTaskExecutor:该实现类不会执行异步的调用所有任务的执行都会发生在调用该executor的线程中。通常使用在不需要多线程的场景
  • SimpleAsyncTaskExecutor该实现类不会复用任何的线程相反的对于每次调用该executor都会使用一个全新的线程。但是该实现类的确支持对并发数量的限制该executor会阻塞任何超过并发数量限制的调用直到slot被释放为止。SimpleAsyncTaskExecutor并不支持池化技术
  • ConcurrentTaskExecutor该实现类是java.util.concurrent.Executor实例的adapter其可以将java.util.concurrent.Executor实例的配置参数以bean properties的形式暴露。当ThreadPoolTaskExecutor的灵活性无法满足需求时可以使用ConcurrentTaskExecutor
  • ThreadPoolTaskExecutor该实现类型是最广泛被使用的。该实现类可以通过bean properties来配置java.util.concurrent.ThreadPoolExecutor实例并且将该实例wrap在TaskExecutor实例中。当想要使用另一种java.util.concurrent.Executor时可以使用ConcurrentTaskExecutor。
  • DefaultManagedTaskExecutor该实现使用了通过JNDI获取的ManagedExecutorService

TaskExecutor的使用

在springboot中当使用@EnableAsync时可以通过配置ThreadPoolExecutor的bean properties来配置线程池的核心线程数和最大线程数等属性。

# 该线程池最多含有16个线程
spring.task.execution.pool.max-size=16
# 有有界队列存放task上限为100
spring.task.execution.pool.queue-capacity=100
# 当线程空闲10s默认60s时会进行回收
spring.task.execution.pool.keep-alive=10s
# 设置核心线程数量
spring.task.execution.pool.core-size=8

Spring TaskScheduler Abstraction

为了在特定的时间点执行taskSpring引入了TaskScheduler接口接口定义如下

public interface TaskScheduler {

    ScheduledFuture schedule(Runnable task, Trigger trigger);

    ScheduledFuture schedule(Runnable task, Instant startTime);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);

scheduleAtFixedRate和scheduleAtFixedDelay区别

  • fixed rate表示两个task执行开始时间的间隔
  • fixed delay表示上一个task结束时间和下一个task开始时间的间隔

实例如下:

  • fixed rateTTWWWTTTWWT...(开始时间间隔为5)
  • fixed delayTTWWWWWTTTTWWWWWTTTTTTTWWWWWT...(上次结束和下次开始之间的间隔为5)

通常TaskScheduler默认情况下是单线程执行的故而fixed rate执行时如果一个Task执行时间超过period时在当前task执行完成之前下一个task并不会开始执行。下一个task会等待当前task执行完成之后立马执行。

Trigger接口

Trigger接口的核心理念是下次执行事件由上次执行的结果决定。上次执行的结果存储在TriggerContext中。
Trigger接口如下

public interface Trigger {

    Date nextExecutionTime(TriggerContext triggerContext);
}

TriggerContext接口如下其具有默认的实现类SimpleTriggerContext

public interface TriggerContext {

    Date lastScheduledExecutionTime();

    Date lastActualExecutionTime();

    Date lastCompletionTime();
}

Trigger实现类

Spring为Trigger提供了两个实现类其中CronTrigger允许task的调度按照cron expression来执行类似linux中的crond

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));

Spring的另一个Trigger实现是PeriodicTrigger其接受一个固定的period期间一个可选的初始delay值并接收一个boolean值标识该period是fixed-rate还是fixed-delay。

TaskScheduler实现类

如果不需要外部的线程管理可以使用spring提供的ThreadPoolTaskScheduler其会将任务委托给ScheduledExecutorService来提供bean-properties形式的配置。

对任务调度和异步执行的注解支持

Spring同时对任务调度和异步方法的执行提供注解支持。

启用Scheduling注解

想要启用@Async和@Scheduled注解必须将@EnableAsync注解和@EnableScheduling注解添加到一个@Configuration类上。

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

@Async注解的实现是通过proxy模式来实现的故而如果在类内部调用位于同一个类中的@Async方法那么代理拦截会失效此时调用的@Async方法将会同步执行而非异步执行

@Async可以接收一个value值用于指定目标的执行器Executor或TaskExecutor的beanName

@Scheduled注解

在将@Scheduled注解标注到方法时可以为其指定一个trigger的元数据使用示例如下

@Scheduled(fixedDelay = 5000)
public void doSomething() {
    // something that should run periodically
}

默认情况下fixedDelay、fixedRate、initialDelay的时间单位都是ms可以指定timeUnit来指定其他的时间单位

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
   // something that should run periodically
}

可以为@Scheduled注解使用cron表达式

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
    // something that should run on weekdays only
}

@Async注解

通过为方法指定@Async注解可以让该方法异步执行该方法的执行通过TaskExecutor。

@Async
void doSomething() {
    // this will be run asynchronously
}

可以为@Async标注的方法指定参数和返回值但是异步方法的返回值只能为void或是Future类型。
在该异步方法的调用方调用返回Future实例的get方法之前调用方仍然能够执行其他操作异步方法的执行位于TaskExecutor的线程中。

@Async
Future<String> returnSomething(int i) {
    // this will be run asynchronously
}

不能将@Async注解和生命周期回调例如@PostConstruct进行混用如果想要异步的初始化bean对象需要按照如下方法

public class SampleBeanImpl implements SampleBean {

    @Async
    void doSomething() {
        // ...
    }

}

public class SampleBeanInitializer {

    private final SampleBean bean;

    public SampleBeanInitializer(SampleBean bean) {
        this.bean = bean;
    }

    @PostConstruct
    public void initialize() {
        bean.doSomething();
    }

}

@Async方法的异常处理

当@Async方法的返回类型是Future类型时处理async方法执行时抛出的异常非常简单当在Future对象上调用get方法时异常会被抛出。但当@Async方法的返回类型是void时执行时抛出的异常既无法被捕获也无法被传输。可以指定一个AsyncUncaughtExceptionHandler来处理此类异常。

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        // handle exception
    }
}

Cron表达式

在Spring中cron表达式格式如下
* * * * * *
其代表为s,min,hour,day of month/*(131)*/,month/*(112)*/,day of week/*(0~7,0 or 7 is sun)*/)

规则如下:

  • 所有字段都可以用(*)来匹配所有值
  • 逗号(,)可以用来分隔同一字段中多个值
  • 分号(-可以用来指定范围指定的范围左右都包含eg1-5代表[1,5]
  • 在范围(或是*)后跟随下划线(/)代表间隔,如在分钟字段指定*/20代表该小时内每过20min
  • 对于月份或者day of week可以使用英文名的前三个字母来代替大小写不敏感
  • 在day of month或day of week字段中可以包含L字母
    • 在day of month字段L代表该月的最后一天在该字段还可以为L指定一个负的偏移量如L-n代表该月的倒数第n+1天
    • 在day of week字段L代表该周的最后一天L前还可以前缀月份的数字或是月份的前三个字母dL或DDDL如7L或sunL代表该月份的最后一个星期日),代该月份的最后一个day of week
  • day of month字段可以指定为nW代表离day of month为n最近的一个工作日如果n为周六则该该字段代表的值为周五的day of month如果n为周六则该字段代表下周一的day of month如果n为1其位于周六其也代表下周一的day of month1W代表该月的第一个工作日
  • 如果day of month的值为LW则代表该月的最后一个工作日
  • day of week字段还能指定为d#n或DDD#n的形式代表该月的的第几个d in week例如SUN#2代表当前月的第二个星期日

Macros

Cron表达式可读性不太好可以使用如下预定义的宏

Macro meaning
@yearly (or @annually) once a year (0 0 0 1 1 *)
@monthly once a month (0 0 0 1 * *)
@weekly once a week (0 0 0 * * 0)
@daily (or @midnight) once a day (0 0 0 * * *), or
@hourly once an hour, (0 0 * * * *)