分析
在SpringBoot的日常开发中,一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
- 第一个原因:容错问题,如果送积分出现异常,不能因为送积分而导致用户注册失败。
- 第二个原因:提升性能,比如注册用户花了30毫秒,送积分花了50毫秒,如果同步的话一共耗时:80毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。
什么是同步和异步
串行执行(同步)
@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 短信发送:100ms 、添加积分:100ms 总时长:250ms 这个方法执行完毕。
同步执行:相当于接力跑步
异步执行
分析:它执行用户注册的执行时长,并不会因为短信发送、添加积分受到影响。执行时间:50ms
异步执行:比如我们登录的时候,需要发送短信,还要添加积分,这两个步骤交由异步去做,可以减少登录所需的时间(在后台开线程去执行)。
异步编程–用户注册
步骤1:让springboot框架支持异步处理
首先要开启@EnableAsync注解,让SpringBoot支持异步执行
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);
}
}
步骤2:定义个异步处理的注册service
package com.kuangstudy.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
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:调用异步处理
package com.kuangstudy.controller;
import com.kuangstudy.service.RegService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RegController {
@Autowired
private RegService regService;
@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
线程池。该线程池不是真正意义上的线程(https://github.com/Async)
因为线程不重用,每次调用都会新建一个新的线程。
通过上面的日志分析获得结论:【task-1】,【task-2】,【task-3】….递增。
[@Async]注解异步框架提供多种线程机制:
- SimpleAsyncTaskExecutor:简单的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
- SyncTaskExecutor:这个类没实现异步调用,只是一个同步操作,只适合用于不需要多线程的地方。
- ConcurrentTaskExecutor:Executor的适配类,不推荐使用.。
- ThreadPoolTaskScheduler:可以和cron表达式使用。
- ThreadPoolTaskExecutor:最常用,推荐,其本质就是:java.util.concurrent.ThreadPoolExecutor的包装
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;
@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;
}
}
什么样子的场景下会用到异步呢?
请注意:异步虽好但是不能泛滥使用。大部分开发中,还是串行执行。除非你在开发过程中,一个业务和另外一个业务的关联性不是强耦合,执行失败或者成功都不影响它核心业务。你可以把这些附属业务剥离处理用异步执行。比如:用户注册:发送短信,发送邮件, 比如:下单成功发送短信,发送微信登等
异步编程的框架:消息中间件(ActiveMQ、RabbitMQ)
思考
用户注册的核心点:就是把用户注册到数据库中。至于你短信有发送成功,有影响。其实不影响?
问:短信发送不成功,那不是注册失败吗?
我们用短信的目的其实就是为了:校验手机的合法性以及激活仅此而已。如果收不到其实不应该去影响注册用户。大不了。我在发一次,或者改天在来发送。但是至少不会让你重新在去填写。