目录
[2.1 @Async注解](#2.1 @Async注解)
定义异步任务类并使用@Component标记组件被容器扫描,异步方法加上@Async
[2.2 @Async注解的弊端](#2.2 @Async注解的弊端)
[2.3 自定义线程池](#2.3 自定义线程池)
[2.4 实践出真知-线程池多参数调整-现象报告对比分析](#2.4 实践出真知-线程池多参数调整-现象报告对比分析)
[异步发送 + resttemplate未池化(小参数)](#异步发送 + resttemplate未池化(小参数))
三、高并发下异步请求解决方案二:RestTemplate池化
[3.1 RestTemplate的弊端Broken pipe](#3.1 RestTemplate的弊端Broken pipe)
[3.2 重新认识RestTemplate](#3.2 重新认识RestTemplate)
[3.3 高性能RestTemplate封装配置实战](#3.3 高性能RestTemplate封装配置实战)
[3.4 【10倍+QPS提升】Jmeter5.x压测 优化后RestTemplate前后性能对比](#3.4 【10倍+QPS提升】Jmeter5.x压测 优化后RestTemplate前后性能对比)
[四、异步@Async自定义线程池 + RestTemplate池化顶级优化策略](#四、异步@Async自定义线程池 + RestTemplate池化顶级优化策略)
一、模拟并发实战环境

这是一段非常简单的代码,采用RestTemplate调用的我本地另一个服务的接口,这里我们选择Jmeter来压测此方法接口。

总共调用10万次接口,我们再来看看聚合结果。

我们发现调用接口失败率为2.27%,这个已经不低了,下面我们使用几种常见的优化方式来解决这个问题。
二、高并发下异步请求解决方案一:异步请求
2.1 @Async注解
主启动类加注解

定义异步任务类并使用@Component标记组件被容器扫描,异步方法加上@Async

注意:@Async失效的几种情况
-
注解@Async的方法不是public方法
-
注解@Async的返回值只能为void或者Future
-
注解@Async方法使用static修饰也会失效
-
spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解
-
调用方与被调方不能在同一个类
-
Spring 在扫描bean的时候会扫描方法上是否包含@Async注解,动态地生成一个子类(即proxy代理类),当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用
-
如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个 bean,所以就失效了
-
所以调用方与被调方不能在同一个类,主要是使用了动态代理,同一个类的时候直接调用,不是通过生成的动态代理类调用
-
一般将要异步执行的方法单独抽取成一个类
-
-
类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
-
在Async 方法上标注@Transactional是没用的,但在Async 方法调用的方法上标注@Transactional 是有效的

2.2 @Async注解的弊端
记住,@Async源码中有三大核心参数,记住它你就能顺利干倒面试官,那么是哪三大呢?
(1)核心线程数:8
(2)队列容量:Integer.MAX_VALUE ( 21亿多)
(3)最大线程数:Integer.MAX_VALUE ( 21亿多)

好,记住这三个参数的值,我们再来说说这三个参数都是干啥的:
首先核心线程数8,我们比如同时有多个请求进来,那么先占用默认线程池的核心线程数的8来工作,其余的请求都放在队列中阻塞着,那么如果队列容量21亿个也满了,那么就会启动最大线程数,也就是说,会创建新的线程去执行任务。
好,那么使用默认线程池有没有什么弊端呢?当然有。
无限制的资源消耗
在高并发场景下会导致大量线程被创建,耗尽系统资源。你想想,默认的参数值都是21亿多,那么资源消耗多大啊。
缺乏队列限制
队列容量是21亿,极易出现OOM
难以监控和管理
-
默认线程池难以进行监控和性能调优
-
无法统一管理线程池参数
共享同一线程池
-
所有
@Async方法共享同一个默认线程池 -
一个耗时任务可能阻塞其他异步任务
2.3 自定义线程池
创建自定义线程池的配置类
java
package net.xdclass.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
//线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
//如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
threadPoolTaskExecutor.setCorePoolSize(8);
//缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行
threadPoolTaskExecutor.setQueueCapacity(124);
//最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
//当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
threadPoolTaskExecutor.setMaxPoolSize(64);
//当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
//允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
//如果allowCoreThreadTimeout=true,则会直到线程数量=0
threadPoolTaskExecutor.setKeepAliveSeconds(30);
//spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
//jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
threadPoolTaskExecutor.setThreadNamePrefix("CLAY的Async前缀:");
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
//AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
//DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
//DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
@Async注解引用自定义线程池
java
@Service
@Slf4j
public class NotifyServiceImpl implements NotifyService {
@Autowired
private RestTemplate restTemplate;
@Override
@Async("threadPoolTaskExecutor")
public void testSend() {
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://localhost:8002/abc/test", String.class);
log.info(forEntity.getBody());
}
}

可以看到确实使用了我们自定义的线程池。
2.4 实践出真知-线程池多参数调整-现象报告对比分析
异步发送 + resttemplate未池化(小参数)
java
threadPoolTaskExecutor.setCorePoolSize(4);
threadPoolTaskExecutor.setMaxPoolSize(16);
threadPoolTaskExecutor.setQueueCapacity(32);

QPS少,并且后台出现大量异常错误,因为你队列容量设置的太少,所以导致大量请求被拒绝。
异步发送+resttemplate未池化(大参数)
java
threadPoolTaskExecutor.setCorePoolSize(32);
threadPoolTaskExecutor.setMaxPoolSize(64);
threadPoolTaskExecutor.setQueueCapacity(10000);
//如果等待队列长度为10万,则qps瞬间很高8k+,可能oom
QPS一开始瞬间很高,然后慢慢下降,因为瞬间都进入到了阻塞队列。
自定义线程池的弊端
-
采用异步发送用户体验变好了,但是存在丢失的可能,阻塞队列存储内存中,如果队列长度过多则重启容易出现丢失数据情况
-
采用了异步发送了+阻塞队列存缓冲,刚开始瞬间QPS高,但是后续也降低很多
-
问题是在哪里?消费方角度,提高消费能力
三、高并发下异步请求解决方案二:RestTemplate池化
3.1 RestTemplate的弊端Broken pipe
我们单纯使用RestTemplate去调用接口,也就是同步调用,并发量上来,会出现错误
Caused by: java.io.IOException: Broken pipe。
这是什么意思呢?
-
服务端向前端socket连接管道写返回数据时 链接(pipe)却断开了
-
从应用角度分析,这是因为客户端等待返回超时了,主动断开了与服务端链接
-
连接数设置太小,并发量增加后,造成大量请求排队等待
-
网络延迟,是否有丢包
-
内存是否足够多支持对应的并发量
-
白话说,就类似于客户端请求服务端,你服务端处理问题时间太长了,客户端等不了了,就断开了socket连接,等服务端处理完任务,发现连接通道没了,就报错Broken pipe异常了。
3.2 重新认识RestTemplate
-
RestTemplate是Spring提供的用于访问Rest服务的客户端
-
底层通过使用java.net包下的实现创建HTTP 请求
-
通过使用ClientHttpRequestFactory指定不同的HTTP请求方式,主要提供了两种实现方式
-
SimpleClientHttpRequestFactory(默认)
-
底层使用J2SE提供的方式,既java.net包提供的方式,创建底层的Http请求连接
-
主要createRequest 方法( 断点调试),每次都会创建一个新的连接,每次都创建连接会造成极大的资源浪费,而且若连接不能及时释放,会因为无法建立新的连接导致后面的请求阻塞
-
-
HttpComponentsClientHttpRequestFactory
- 底层使用HttpClient访问远程的Http服务
-
问题解决:
-
RestTemplate是Spring提供的用于访问Rest服务的客户端
-
底层通过使用java.net包下的实现创建HTTP 请求
-
通过使用ClientHttpRequestFactory指定不同的HTTP请求方式,主要提供了两种实现方式
-
SimpleClientHttpRequestFactory(默认)
-
底层使用J2SE提供的方式,既java.net包提供的方式,创建底层的Http请求连接
-
主要createRequest 方法( 断点调试),每次都会创建一个新的连接,每次都创建连接会造成极大的资源浪费,而且若连接不能及时释放,会因为无法建立新的连接导致后面的请求阻塞
-
-
HttpComponentsClientHttpRequestFactory
- 底层使用HttpClient访问远程的Http服务
-
3.3 高性能RestTemplate封装配置实战
java
package net.xdclass.config;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}
/**
* @return
*/
@Bean
public HttpClient httpClient() {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
//设置整个连接池最大连接数
connectionManager.setMaxTotal(500);
//MaxPerRoute路由是对maxTotal的细分,每个主机的并发,这里route指的是域名
connectionManager.setDefaultMaxPerRoute(200);
RequestConfig requestConfig = RequestConfig.custom()
//返回数据的超时时间
.setSocketTimeout(20000)
//连接上服务器的超时时间
.setConnectTimeout(10000)
//从连接池中获取连接的超时时间
.setConnectionRequestTimeout(1000)
.build();
return HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.build();
}
}
3.4 【10倍+QPS提升】Jmeter5.x压测 优化后RestTemplate前后性能对比
同步发送+resttemplate未池化
- 压测结果 几百 吞吐量
同步发送+resttemplate池化
- 压测结果

同步请求的QPS居然也能达到近3000的吞吐量。
四、异步@Async自定义线程池 + RestTemplate池化顶级优化策略
最终我们可以将两个方案结合到一起,就非常完美了,比如我们发短信功能,就可以采用二者结合到一起的方案:
java
package net.xdclass.component;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.config.SmsConfig;
import net.xdclass.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
@Slf4j
public class SmsComponent {
/**
* 发送地址
*/
private static final String URL_TEMPLATE = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
@Autowired
private RestTemplate restTemplate;
@Autowired
private SmsConfig smsConfig;
@Async("threadPoolTaskExecutor")
public void send(String to, String templateId, String value) {
long beginTime = CommonUtil.getCurrentTimestamp();
String url = String.format(URL_TEMPLATE, to, templateId, value);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "APPCODE " + smsConfig.getAppCode());
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
long endTime = CommonUtil.getCurrentTimestamp();
log.info("耗时={}, url={}, body={}", endTime - beginTime, url, response.getBody());
log.info("url={},body={}", url, response.getBody());
if (response.getStatusCode() == HttpStatus.OK) {
log.info("发送短信成功,响应信息:{}", response.getBody());
} else {
log.error("发送短信失败,响应信息:{}", response.getBody());
}
}
}