在SpringBoot的日常开发中,一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
同步:按顺序执行,前面结束了,后面才开始,是一个串行调用
异步:按顺序执行,但是后面拿到开始不用等待前面结束
举个例子:用户注册,那就送积分和发短信
用户注册:50MS 短信发送:100ms 、添加积分:100ms 总时长:250ms
package com.kuangstudy.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description:
* @author: xuke
* @time: 2021/6/23 20:10
*/
@RestController
@Slf4j
public class RegController {
/**
* @Author xuke
* @Description 用户注册
* @Date 20:11 2021/6/23
* @Param []
* @return java.lang.String
**/
@GetMapping("/reg")
public String reguser(){
// 1: 注册用户
log.info("新用户注册");
//userService.save(user);
// 2: 发送短信
log.info("发送短信");
//messageService.sendMsg(user);
// 3: 添加积分
log.info("添加积分");
//scoreService.addScore(user);
return "ok";
}
}
分析:它执行用户注册的执行时长,并不会因为短信发送、添加积分收到影响。执行时间:>50ms
第一步:开启异步支持(在启动类或者线程池配置类上加注解)
package com.kuangstudy;
import com.kuangstudy.config.WeixinPayProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableAsync;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableAsync //开启异步执行
public class KuangstudySpringbootProApplication {
public static void main(String[] args) {
SpringApplication.run(KuangstudySpringbootProApplication.class, args);
}
}
第二步:方法上添加@Async
让方法变成异步方法
package com.kuangstudy.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
/**
* @description:
* @author: xuke
* @time: 2021/6/23 20:37
*/
@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();
}
}
}
第三步:
调用异步任务
package com.kuangstudy.controller;
import com.kuangstudy.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;
/**
* @description:
* @author: xuke
* @time: 2021/6/23 20:10
*/
@RestController
@Slf4j
public class RegController {
@Autowired
private RegService regService;
/**
* @Author xuke
* @Description 用户注册
* @Date 20:11 2021/6/23
* @Param []
* @return java.lang.String
**/
@GetMapping("/reg")
public String reguser(){
// 1: 注册用户 10ms
log.info("新用户注册");
//userService.save(user);
// 2: 发送短信 5s
log.info("发送短信");
regService.sendMsg();
// 3: 添加积分 5s
log.info("添加积分");
regService.addScore();
return "ok";
}
}
执行的时间很快,因为添加积分和发送短信都是额外的开启了新的线程去执行。所有异步编程的性能是很快的。
Springboot的tomcat的线程默认数量:200个,如果异步线程线程过多,有请求线程、异步处理的线程这个时候,这么线程都在争抢CPU的执行时间。这样很耗费资源 ;而且@ Async注解默认情况下用的是SimpleAsyncTaskExecutor
线程池.[。该线程池不是真正意义上的线程
不是真正意义上的线程,因为线程不重用,每次调用都会新建一个新的线程。
通过上面的日志分析获得结论:【task-1】,【task-2】,【task-3】….递增。
这样每次都创建一个新的线程,太小太消耗资源了,所有我们要把@ Async使用的线程池变成真正意义下的线程池(不会每次都创建)。
@Async(这是我的另一文章)注解异步框架提供多种线程机制:
从上可以看出,我们推荐使用最后面的哪个线程池ThreadPoolTaskExecutor
优化
优化的就是定义一个ThreadPoolTaskExecutor线程池的配置类,把默认的 SimpleAsyncTaskExecutor线程池覆盖掉。从而让他不会每次都创建新的线程。
其实就是定义一个线程池,spring机制就会自动覆盖掉异步以前默认端线程池。
package com.kuangstudy.config;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @description:
* @author: xuke
* @time: 2021/6/1 21:32
*/
@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("ksdsysn-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;
}
}
思考
用户注册的核心点:就是把用户注册到数据库中。至于你短信有发送成功,有影响。其实不影响?
问:短信发布不成功,那不是注册失败吗?
我们用短信的目的其实就是为了:校验手机的合法性以及激活仅此而已。如果收不到其实不应该去影响注册用户。大不了。我在发一次,或者改天在来发送。但是至少不会让你重新在去填写。
在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,其实,在spring 3.x之后,就已经内置了@Async来完美解决这个问题,本文将完成介绍@Async的用法。
同步:按顺序执行,前面结束了,后面才开始,是一个串行调用
异步:按顺序执行,但是后面拿到开始不用等待前面结束
在Java中,一般在处理类似的场景之时,都是基于创建独立的线程去完成相应的异步调用逻辑,通过主线程和不同的线程之间的执行流程,从而在启动独立的线程之后,主线程继续执行而不会产生停滞等待的情况。
在Spring中,基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。
@EnableAsync就可以使用多线程。使用@Async就可以定义一个线程任务
第一步:在定义了线程池的属性类上加@EnableAsync,开启对异步的支持,也就是多线程的使用
这种方法的异步线程池,不能处理抛出来的异常,要用下面的拓展:线程池的另外一种配置来处理异常;
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
private static final int corePoolSize = 10; // 核心线程数(默认线程数)
private static final int maxPoolSize = 100; // 最大线程数
private static final int keepAliveTime = 10;// 允许线程空闲时间(单位:默认为秒)
private static final int queueCapacity = 200;// 缓冲队列数
private static final String threadNamePrefix = "Async-Service-"; // 线程池名前缀
@Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名
public ThreadPoolTaskExecutor getAsyncExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveTime);
executor.setThreadNamePrefix(threadNamePrefix);
// 线程池对拒绝任务的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}
第二步:需要异步的方法(任务)上面加上@Async
@Service
public class testAsyncService {
Logger log = LoggerFactory.getLogger(testAsyncService.class);
// 发送提醒短信 1
@Async("taskExecutor") // 给线程池命名了就得加上注解里面的属性,没有就不需要
public void service1() throws InterruptedException {
log.info("--------start-service1------------");
Thread.sleep(5000); // 模拟耗时
log.info("--------end-service1------------");
}
// 发送提醒短信 2
@Async("taskExecutor")
public void service2() throws InterruptedException {
log.info("--------start-service2------------");
Thread.sleep(2000); // 模拟耗时
log.info("--------end-service2------------");
}
}
@Async注解来声明一个或多个异步任务,可以加在方法或者类上,加在类上表示这整个类都是使用这个自定义线程池进行操作
--------start-service1------------
--------start-service2------------
--------end-service2------------
--------end-service1------------
可以说明我们的异步运行成功了
以上是无返回值的异步任务,使用起来很简单,那有返回值的异步认为u如何处理呢?
对于有返回值的异步任务,我们需要注意以下两点:
代码:
// 异步方法
@Async
public Future asyncMethodWithReturnType() {
System.out.println("Execute method asynchronously - "
+ Thread.currentThread().getName());
try {
Thread.sleep(5000);
return new AsyncResult("hello world !!!!"); // 返回
} catch (InterruptedException e) {
return null;
}
}
public void testAsyncAnnotationForMethodsWithReturnType()
throws InterruptedException, ExecutionException {
System.out.println("Invoking an asynchronous method. "
+ Thread.currentThread().getName());
Future future = asyncAnnotationExample.asyncMethodWithReturnType();
while (true) { ///这里使用了循环判断,等待获取结果信息
if (future.isDone()) { //判断是否执行完毕
System.out.println("Result" + future.get());
break;
}
System.out.println("Continue doing something else. ");
Thread.sleep(1000);
}
}
这些获取异步方法的结果信息,是通过不停的检查Future的状态来获取当前的异步方法是否执行完毕来实现的
异步方法使用static修饰
异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类(因为@Async是spring的注解)
类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象(就以上例来说,得注入service,而不能new)
如果使用SpringBoot框架必须在启动类中/或者线程池固定属性类中,增加@EnableAsync注
在Async 方法上标注@Transactional是没用的。 在Async 方法调用的方法上标注@Transactional 有效。
调用被@Async标记的方法的调用者不能和被调用的方法在同一类中不然不会起作用!!!!!!!
使用@Async时要求是不能有直接的返回值(void或者futre)的不然会报错的 因为异步要求是不关心结果的
在@Async标注的方法,同时也适用了@Transactional进行了标注;在其调用数据库操作之时,将无法产生事务管理的控制,原因就在于其是基于异步处理的操作。
那该如何给这些操作添加事务管理呢?可以将需要事务管理操作的方法放置到异步方法内部,在内部被调用的方法上添加@Transactional.
例如:
方法A,使用了@Async/@Transactional来标注,但是无法产生事务控制的目的。
方法B,使用了@Async来标注, B中调用了C、D,C/D分别使用@Transactional做了标注,则可实现事务控制的目的。
这种异步线程池的配置可以实现对异常的处理;
AsyncConfigurer 接口中的方法 public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() 用于处理异步方法的异常。
AsyncUncaughtExceptionHandler 接口,只有一个方法:void handleUncaughtException(Throwable ex, Method method, Object… params); 所以可以使用lambda表达式
- 在无返回值的异步调用中,异步处理抛出异常,AsyncExceptionHandler的handleUncaughtException()会捕获指定异常,原有任务还会继续运行,直到结束。
- 在有返回值的异步调用中,异步处理抛出异常,会直接抛出异常,异步任务结束,原有处理结束执行。
如果使用threadPoolTaskExecutor()来定义bean,则不需要初始化。
下面关于线程池的配置还有一种方式,就是直接实现AsyncConfigurer接口,重写getAsyncExecutor方法即可,代码如下
@Configuration
@EnableAsync
public class AppConfig implements AsyncConfigurer {
// @Bean
// public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
// ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// executor.setCorePoolSize(10);
// executor.setMaxPoolSize(100);
// executor.setQueueCapacity(100);
// return executor;
// }
// 自定义线程池,控制并发数,将线程池的大小设置成只有10个线程
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize(); //如果不初始化,导致找到不到执行器
return executor;
}
// 统一处理异常
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SpringAsyncExceptionHandler();
}
class SpringAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
/*PcLog pcLog = new PcLog();
if (throwable instanceof PlanException) {
Integer type = ((PlanException) throwable).getType();
pcLog.setLogCode(type);
EamErrorType eamErrorType = EamErrorType.fromValue(type);
pcLog.setCodeName(eamErrorType.name());
} else {
pcLog.setLogCode(-1);
pcLog.setCodeName(throwable.getClass().getName());
}
try {
if (throwable.getMessage() == null) {
pcLog.setErrorMessage(throwable.getClass().getName());
} else {
pcLog.setErrorMessage(throwable.getMessage().length() > 500?throwable.getMessage().substring(0,500): throwable.getMessage());
}
StackTraceElement[] stackTrace = throwable.getStackTrace();
StringBuilder sb = new StringBuilder();
for (StackTraceElement element : stackTrace) {
sb.append(element.toString());
sb.append("\n");
}
pcLog.setTrace(sb.toString());
} catch (Exception e) {
String errMsg = MyPlanConstant.LOGINFO + MyPlanConstant.DATAPK;
logger.error(errMsg);
throw new PlanException(EamErrorType.PC_SQL_OPERATE_ERROR.getValue(), errMsg);
}
insertPcLog(method.getName(), obj, null, pcLog);*/
}
}
/*public void insertPcLog(String methodName, Object[] paramArgs, String ip, PcLog pcLog) {
try {
Long userId = PermissionAspect.getUserId();
pcLog.setAccessorId(userId); // 调用者
pcLog.setAccessorName(myCommonUtil.getUserById(userId.toString()).getString("nickName"));
} catch (Exception e) {
logger.error(e.getMessage() , e);
throw new PlanException(EamErrorType.PC_USER_TOKEN_ERROR.getValue(), MyPlanConstant.PC_USER_TOKEN_ERROR);
}
try {
pcLog.setAccessorType("USER");
pcLog.setPortName(methodName);
pcLog.setIp(ip);
pcLog.setCreateTime(new Date());
pcLog.setProps(Arrays.toString(paramArgs));
logMapper.insert(pcLog);
} catch (Exception e) {
String errMsg = MyPlanConstant.PC_LOG_INSERT_ERROR;
logger.error(errMsg);
throw new PlanException(EamErrorType.PC_LOG_INSERT_ERROR.getValue(), errMsg);
}
}*/
}
2.1、在无返回值的异步调用中,异步处理抛出异常,AsyncExceptionHandler的handleUncaughtException()会捕获指定异常,原有任务还会继续运行,直到结束。
2.2、在有返回值的异步调用中,异步处理抛出异常,会直接抛出异常,异步任务结束,原有处理结束执行。
正常来说在调用异步方法的外面捕获异常,那就可以抛出,不要在异步方法中使用异常捕获trycatch
文章
见上
假如异步任务的返回作为后面业务逻辑的条件,那就需要使用带有返回参数的异步方法的格式。
因为异步的情况,主线程不会等待其他线程走完就返回数据给前端了,所以可以配合计数器来确保线程执行完毕再返回
但是这样不能捕获的异步方法的异常,因为出现异常后计数器就不会减一,导致await方法就阻塞一直的等待了;
可以用下面这个所有方法实现异步方法都走完才走主线程的方法
class a {
@Async
public CompletableFuture checkMaterialsByScreen() {
// 业务逻辑
return CompletableFuture.completedFuture("success");
}
@Async
public CompletableFuture checkMaterialUsed() {
// 业务逻辑
return CompletableFuture.completedFuture("success");
}
}
class b {
public void test() {
CompletableFuture a = myCommonUtil.checkMaterialsByScreen();// 校验素材是否满足分辨率
CompletableFuture b = myCommonUtil.checkMaterialUsed();// 节目单和上换刊素材校验是否重
CompletableFuture.allOf(a,b).join();}
// 其他业务逻辑
}
但是需要注意的是,此时,你上面设置的异步异常处理就是失效了,得整体try/catch来自己捕获和抛出异常了