SpringBoot中的异步处理框架@Async

本文最后更新于:3 年前

异步处理框架@Async在SpringBoot中的使用

雪女

@Async异步处理框架

分析

在SpringBoot的日常开发中,一般都是==同步==调用的,但经常有特殊业务需要做==异步==来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。

优势

  • 第一个原因:容错问题,如果送积分出现异常,不能因为送积分而导致用户注册失败。
  • 第二个原因:提升性能,比如注册用户花了30毫秒,送积分划分50毫秒,如果同步的话一共耗时:70毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。

同步执行和异步执行

同步执行(串行执行)

==代码顺序执行==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @description: some desc
* @author: septzhang
* @email: 1339126726@qq.com
* @Date: 2021/8/30 15:47
*/
@RestController
@Slf4j
public class RegController {
@Autowired
private RegService regService;
/**
* @return java.lang.String
* @Author septzhang
* @Email 1339126726@qq.com
* @Description //TODO
* @Date 16:09 2021/8/30
* @Param []
**/
@GetMapping("/reg")
public R register() {
//1: 新用户注册
log.info("用户注册");
regService.sendMsg();
//2: 发送短信
log.info("发送短信");
regService.addScore();
//3: 添加积分
return R.success("OK");
}
}

异步执行示意图

问题:

串行执行的时长:是所有方法执行的总和。

打个比方:

用户注册:50MS

短信发送:100ms

添加积分:100ms

总时长:250ms

完毕

异步执行

同步执行示意图

问题:异步执行的时长:是最后一个方法执行完成的时间。

打个比方:

用户注册:50MS

短信发送:100ms

添加积分:100ms

总时长:100ms

完毕。

使用

开启异步执行

1
2
3
4
5
6
7
@SpringBootApplication
@EnableAsync //开启异步执行
public class StudyBootsProApplication {
public static void main(String[] args) {
SpringApplication.run(StudyBootsProApplication.class, args);
}
}

定义异步处理的service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
*@ClassName ResService
*@Author septzhang
*@Date 2021/8/30 16:29
*@Version 1.0
*@Description 异步处理的注册service
**/
@Service
@Slf4j
public class RegService {
/**
* @Author septzhang
* @Email 1339126726@qq.com
* @Description // 发送短信用异步进行处理和标记
* @Date 16:36 2021/8/30
* @Param []
* @return void
**/
@Async
public void sendMsg(){
//todo:模拟耗时5s
try{
Thread.sleep(5000);
log.info("------------发送短信--------------");
}catch (Exception e){
e.printStackTrace();
}
}
@Async
public void addScore(){
try{
Thread.sleep(3000);
log.info("-------处理积分-------");
}catch (Exception e){
e.printStackTrace();
}

}
}

调用异步处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @description: some desc
* @author: septzhang
* @email: 1339126726@qq.com
* @Date: 2021/8/30 15:47
*/
@RestController
@Slf4j
public class RegController {
@Autowired
private RegService regService;
/**
* @return java.lang.String
* @Author septzhang
* @Email 1339126726@qq.com
* @Description //TODO
* @Date 16:09 2021/8/30
* @Param []
**/
@GetMapping("/reg")
public R register() {
//1: 新用户注册
log.info("用户注册");
regService.sendMsg();
//2: 发送短信
log.info("发送短信");
regService.addScore();
//3: 添加积分
return R.success("OK");
}
}

运行结果

1
2
3
4
2021-08-30 20:48:00.990  INFO 8348 --- [nio-8081-exec-1] com.ajie.controller.RegController        : 用户注册
2021-08-30 20:48:00.996 INFO 8348 --- [nio-8081-exec-1] com.ajie.controller.RegController : 发送短信
2021-08-30 20:48:04.018 INFO 8348 --- [-async-thread-2] com.ajie.sevice.RegService : -------处理积分-------
2021-08-30 20:48:06.016 INFO 8348 --- [-async-thread-1] com.ajie.sevice.RegService : ------------发送短信--------------

异步线程池的优化

==Springboot的tomcat的线程默认数量:200个,如果异步线程线程过多,有请求线程、异步处理的线程这个时候,这么线程都在争抢CPU的执行时间。这样很耗费资源 ,因为@Async](https://github.com/Async)注解默认情况下用的是`SimpleAsyncTaskExecutor`线程池.[该线程池不是真正意义上的线程】==

因为线程不重用,每次调用都会新建一个新的线程。

通过上面的日志分析获得结论:【task-1】,【task-2】,【task-3】….递增。

@Async注解异步框架提供多种线程机制:

  • SimpleAsyncTaskExecutor:简单的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
  • SyncTaskExecutor:这个类没实现异步调用,只是一个同步操作,只适合用于不需要多线程的地方。
  • ConcurrentTaskExecutor:Executor的适配类,不推荐使用.。
  • ThreadPoolTaskScheduler:可以和cron表达式使用。
  • ThreadPoolTaskExecutor:最常用,推荐,其本质就是:java.util.concurrent.ThreadPoolExecutor的包装

配置SyncThreadPoolConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
*@ClassName SyncThreadPoolConfiguration
*@Author septzhang
*@Date 2021/8/30 16:56
*@Version 1.0
*@Description 配置SyncThreadPoolConfiguration
**/
@Configuration
public class SyncThreadPoolConfiguration {

/**
* @Author septzhang
* @Email 1339126726@qq.com
* @Description // 把springboot中的默认的异步线程线程池给覆盖掉。用ThreadPoolTaskExecutor来进行处理
* @Date 17:00 2021/8/30
* @Param []
* @return org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
**/
@Bean(name = "thrandPoolTaskExecutor")
public ThreadPoolTaskExecutor getThreadPoolTaskExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();

//1: 创建核心线程数 cpu核数 -- 50
threadPoolTaskExecutor.setCorePoolSize(10);

//2: 线程池维护线程的最大数量, 只有在 缓存队列 满了之后, 才会申请超过 核心线程数 的线程
threadPoolTaskExecutor.setMaxPoolSize(100);

//3: 缓存队列, 可以写大一些,无非是占用一些内存空间
threadPoolTaskExecutor.setQueueCapacity(200);

//4: 线程的空闲时间, 当超过了 核心线程数的线程 在到达 指定空闲时间 后,会被 销毁(ms)
threadPoolTaskExecutor.setKeepAliveSeconds(200);
//5: 异步方法内部线程的名称 前缀
threadPoolTaskExecutor.setThreadNamePrefix("ajie-async-thread-");

//6: 缓存队列的策略 多线程 JUC并发
/* 当线程的任务缓存队列已满并且线程池中的线程数量已经达到了最大连接数,如果还有任务来就会采取拒绝策略,
* 通常有四种策略:
*ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常:RejectedExcutionException异常
*ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
*ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
*ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用execute()方法,直到成功。
*ThreadPoolExecutor. 扩展重试3次,如果3次都不成功再移除。
*jmeter 压力测试 1s=500
* */
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.initialize();

return threadPoolTaskExecutor;
}
}

使用场景

请注意:异步虽好但是不能泛滥使用。大部分开发中,还是串行执行。

除非在开发过程中,一个业务和另外一个业务的关联性不是强耦合,执行失败或者成功都不影响它核心业务。你可以把这些附属业务剥离处理用异步执行。

比如:用户注册:发送短信,发送邮件, 比如:下单成功发送短信,发送微信登等

异步编程的框架:消息中间件(ActiveMQ、RabbitMQ)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!