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

239 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [Spring Boot Async](#spring-boot-async)
- [Spring Executor和Scheduler的自动配置](#spring-executor和scheduler的自动配置)
- [Task Execution and Scheduling](#task-execution-and-scheduling)
- [Task Execution Abstraction](#task-execution-abstraction)
- [TaskExecutor接口的实现种类](#taskexecutor接口的实现种类)
- [TaskExecutor的使用](#taskexecutor的使用)
- [Spring TaskScheduler Abstraction](#spring-taskscheduler-abstraction)
- [Trigger接口](#trigger接口)
- [Trigger实现类](#trigger实现类)
- [TaskScheduler实现类](#taskscheduler实现类)
- [对任务调度和异步执行的注解支持](#对任务调度和异步执行的注解支持)
- [启用Scheduling注解](#启用scheduling注解)
- [@Scheduled注解](#scheduled注解)
- [@Async注解](#async注解)
- [@Async方法的异常处理](#async方法的异常处理)
- [Cron表达式](#cron表达式)
- [Macros](#macros宏)
# Spring Boot Async
## Spring Executor和Scheduler的自动配置
当当前上下文中没有Executor类型的bean对象时spring boot会自动配置一个ThreadPoolTaskExecutor类型的bean对象并且将该bean对象和异步task执行@EnableAsync和spring mvc异步请求处理关联在一起。
该默认创建的ThreadPoolTaskExecutor默认使用8个核心线程并且线程数量可以根据负载动态的增加或者减少。
可以通过如下方式对ThreadPoolTaskExecutor进行配置
```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
```
如果使用@EnableScheduling一个ThreadPoolTaskScheduler也可以被配置。该线程池默认使用一个线程但是也可以动态设置
```properties
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来配置线程池的核心线程数和最大线程数等属性。
```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接口接口定义如下
```java
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接口如下
```java
public interface Trigger {
Date nextExecutionTime(TriggerContext triggerContext);
}
```
TriggerContext接口如下其具有默认的实现类SimpleTriggerContext
```java
public interface TriggerContext {
Date lastScheduledExecutionTime();
Date lastActualExecutionTime();
Date lastCompletionTime();
}
```
### Trigger实现类
Spring为Trigger提供了两个实现类其中CronTrigger允许task的调度按照cron expression来执行类似linux中的crond
```java
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类上
```java
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
```
> **@Async注解的实现是通过proxy模式来实现的故而如果在类内部调用位于同一个类中的@Async方法那么代理拦截会失效此时调用的@Async方法将会同步执行而非异步执行**
> @Async可以接收一个value值用于指定目标的执行器Executor或TaskExecutor的beanName
### @Scheduled注解
在将@Scheduled注解标注到方法时可以为其指定一个trigger的元数据使用示例如下
```java
@Scheduled(fixedDelay = 5000)
public void doSomething() {
// something that should run periodically
}
```
> 默认情况下fixedDelay、fixedRate、initialDelay的时间单位都是ms可以指定timeUnit来指定其他的时间单位
> ```java
> @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
> public void doSomething() {
> // something that should run periodically
> }
> ```
可以为@Scheduled注解使用cron表达式
```java
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// something that should run on weekdays only
}
```
### @Async注解
通过为方法指定@Async注解可以让该方法异步执行该方法的执行通过TaskExecutor。
```java
@Async
void doSomething() {
// this will be run asynchronously
}
```
可以为@Async标注的方法指定参数和返回值但是异步方法的返回值只能为void或是Future类型。
在该异步方法的调用方调用返回Future实例的get方法之前调用方仍然能够执行其他操作异步方法的执行位于TaskExecutor的线程中。
```java
@Async
Future<String> returnSomething(int i) {
// this will be run asynchronously
}
```
不能将@Async注解和生命周期回调(例如@PostConstruct进行混用如果想要异步的初始化bean对象需要按照如下方法
```java
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来处理此类异常。**
```java
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/\*(1~31)\*/,month/\*(1~12)\*/,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 * * * *) |