SpringBoot中的异步处理框架@Async
1、分析
在SpringBoot的日常开发中,一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
- 第一个原因:容错问题,如果送积分出现异常,不能因为送积分而导致用户注册失败。
- 第二个原因:提升性能,比如注册用户花了30毫秒,送积分划分50毫秒,如果同步的话一共耗时:70毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。
场景:用户注册 发送短信和添加积分
package com.example.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RegController {
// 注册
@GetMapping("/reg")
public String reguser(){
// 1、注册用户
log.info("新用户注册");
// userService.save(user);
// 2、发送短信
log.info("新用户注册");
// messageService.sendMsg();
// 3、添加积分
log.info("新用户注册");
// scoreService.addScore(user);
return "OK";
}
}
2、什么是同步和异步
2.1 串行执行
// 注册
@GetMapping("/reg")
public String reguser(){
// 1、注册用户
log.info("新用户注册");
// userService.save(user);
// 2、发送短信
log.info("新用户注册");
// messageService.sendMsg();
// 3、添加积分
log.info("新用户注册");
// scoreService.addScore(user);
return "OK";
}
串行执行时长:所有方法执行的总和
用户注册:50MS 短信发送:100ms 、添加积分:100ms
总时长:250ms 这个方法执行完毕。
2.2 异步执行
分析:执行用户注册的执行时长,并不会因为短信发送、添加积分受到影响。执行时间:>50ms
异步执行:方法与方法之间互不影响
3、异步编程–用户注册
3.1 让springboot框架支持异步处理
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync //开启异步执行
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3.2 定义异步处理的注册service
package com.example.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class RegService {
// 发送短信,用异步进行处理和标记
@Async
public void sendMsg(){
// todo :模拟耗时5秒
try {
Thread.sleep(5000);
log.info("---------------发送消息--------");
}catch (Exception ex){
ex.printStackTrace();
}
}
// 添加积分,用异步进行处理和标记
@Async
public void addScore(){
// todo :模拟耗时5秒
try {
Thread.sleep(5000);
log.info("---------------处理积分--------");
}catch (Exception ex){
ex.printStackTrace();
}
}
}
3.3 调用异步处理
package com.example.controller;
import com.example.service.RegService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RegController {
@Autowired
private RegService regService;
// 注册
@GetMapping("/reg")
public String reguser(){
// 1、注册用户
log.info("新用户注册");
// userService.save(user);
// 2、发送短信
log.info("新用户注册");
// messageService.sendMsg();
regService.sendMsg();
// 3、添加积分
log.info("新用户注册");
// scoreService.addScore(user);
regService.addScore();
return "OK";
}
}
3.4 执行结果
执行的时间很快,因为添加积分和发送短信都是额外的开启了新的线程去执行。所有异步编程的性能是很快的。
3.5 小结
在SpringBoot的日常开发中,一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
- 第一个原因:容错问题,如果送积分出现异常,不能因为送积分而导致用户注册失败。
- 第二个原因:提升性能,比如注册用户花了30毫秒,送积分划分50毫秒,如果同步的话一共耗时:70毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。
4、异步线程池的优化
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的包装
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class SyncThreadPoolConfiguration {
/**
* 把springboot中的默认的异步线程线程池给覆盖掉。用ThreadPoolTaskExecutor来进行处理
**/
@Bean(name="threadPoolTaskExecutor")
public ThreadPoolTaskExecutor getThreadPoolTaskExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
// 1: 创建核心线程数 cpu核数 -- 50
threadPoolTaskExecutor.setCorePoolSize(10);
// 2:线程池维护线程的最大数量,只有在缓存队列满了之后才会申请超过核心线程数的线程
threadPoolTaskExecutor.setMaxPoolSize(100);
// 3:缓存队列 可以写大一点无非就浪费一点内存空间
threadPoolTaskExecutor.setQueueCapacity(200);
// 4:线程的空闲事件,当超过了核心线程数之外的线程在达到指定的空闲时间会被销毁 200ms
threadPoolTaskExecutor.setKeepAliveSeconds(200);
// 5:异步方法内部线的名称
threadPoolTaskExecutor.setThreadNamePrefix("example-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;
}
}
5、异步应用场景
大部分开发中,还是串行执行。除非在开发过程中,一个业务和另外一个业务的关联性不是强耦合,执行失败或者成功都不影响它核心业务。可以把这些附属业务剥离处理用异步执行。比如:用户注册:发送短信,发送邮件,下单成功发送短信,发送微信登等
异步编程的框架:消息中间件(ActiveMQ、RabbitMQ)