Async注解使用
- Async注解使用时需要配合@EnableAsync注解。
- Async调用时需要在另外一个类中的public类中调用,这与所有注解一样,因为注解是基于Spring AOP的方式织入代码的。而Spring AOP是通过代理的方式实现的,准确的来说Spring AOP是有两种代理方式,一种是JDK代理,一种是CGLIB代理。
(1)如果目标对象实现了接口,也就是基于接口的编程;默认情况下是采用JDK的动态代理实现AOP。当然可以强制使用CGLIB实现AOP。
(2)如果目标对象没有实现接口,必须采用CGLIB库。
可以总结为:
Spring 5.x 中 AOP 默认依旧使用 JDK 动态代理。
SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。
在 SpringBoot 2.x 中,如果需要默认使用 JDK 动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改,proxyTargetClass配置已无效。
注意:
如果@springbootApplication注解与@ComponentScan、@EnableAsync注解达到相同的功效。
发现问题
线上有客户反映说一直提交不了订单状态。
我们通过代码查看,发现异步前的日志是可以刷出来的,但是异步后的日志就刷不出来。也就是说线上无缘无故就出现了异步方法不能使用了。我们考虑了是不是线程资源不够导致,但是查看进程线程,我们很快就否定了。
top -H -p 26565 //查看进程的线程,粗略发现并没有太多线程,只有78个。
那是什么问题呢?我们进一步分析代码,代码如下:
代码入口:
异步调用的方法:
发现调用前是一个文件中有51次调用。
但是调用后的日志中只有11次打印。
这是为啥有些调动没有开线程呢?我们再来看线程池的设置。什么,队列中有99999个。也就是说如果线程池中的核心线程如果大于5个,新来的请求会放入到队列中,既然队列这么大,几乎能容纳所有大于核心线程之后的请求。也就是说还没来的及开线程。最大线程也就没有用了。
为什么会导致线程全部加入到队列中长时间没有处理呢,这是因为最近几天被DDOS攻击,网络管理员屏蔽了联通的出口IP列表,而第三方配置的白名单全部是通过联通的IP进行通信的,所以,导致HTTP链接访问不了外网,但是为啥不会超时断开呢,看下代码。
ByteArrayEntity entity = new ByteArrayEntity(requestStr.getBytes(StandardCharsets.UTF_8));
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader(HTTP.CONTENT_TYPE, "application/json;charset=utf-8");
httpPost.addHeader("Accept", "application/json");
httpPost.setHeader("ci", creditPlatformProperties.getMerchantId());
httpPost.setHeader("tr", tr);
httpPost.setHeader("cs", cs);
httpPost.setEntity(entity);
//TODO 以下两行是增加的代码
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(2 * 60 * 1000).setConnectionRequestTimeout(1000).setConnectTimeout(2 * 60 * 1000).build();//设置请求和传输超时2分时间
httpPost.setConfig(requestConfig);
log.info("发送风控平台地址:[{}],报文:[{}]", url, requestStr);
httpResponse = httpClient.execute(httpPost);
String inputLine;
reader = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));
while ((inputLine = reader.readLine()) != null) {
response.append(inputLine);
}
这个方法的几个参数的含义:
connectionRequestTimout:指从连接池获取连接的timeout
connetionTimeout:指客户端和服务器建立连接的timeout,
就是http请求的三个阶段,一:建立连接;二:数据传送;三,断开连接。超时后会ConnectionTimeOutException
socketTimeout:指客户端从服务器读取数据的timeout,超出后会抛出SocketTimeOutException
数据传输过程中数据包之间间隔的最大时间
下面重点说下SocketTimeout,比如有如下图所示的http请求
虽然报文(“abc”)返回总共用了6秒,如果SocketTimeout设置成4秒,实际程序执行的时候是不会抛出java.net.SocketTimeoutException: Read timed out异常的。
因为SocketTimeout的值表示的是“a”、”b”、”c”这三个报文,每两个相邻的报文的间隔时间不能超过SocketTimeout。
如下,虽然设置SocketTimeout为6000(6秒),但是程序执行了9秒,也没有抛出java.net.SocketTimeoutException: Read timed out异常。
一次http请求,必定会有三个阶段,一:建立连接;二:数据传送;三,断开连接。
当建立连接在规定的时间内(ConnectionTimeOut )没有完成,那么此次连接就结束了。后续的SocketTimeOutException就一定不会发生。只有当连接建立起来后,
也就是没有发生ConnectionTimeOutException ,才会开始传输数据,如果数据在规定的时间内(SocketTimeOut)传输完毕,则断开连接。否则,触发SocketTimeOutException
/**
* Returns the timeout in milliseconds used when requesting a connection
* from the connection manager. A timeout value of zero is interpreted
* as an infinite timeout.
* A timeout value of zero is interpreted as an infinite timeout. 0表示无限时长
* A negative value is interpreted as undefined (system default). 负值表示与系统链接的超时时间一致
* Default: {@code -1}
*/
public int getConnectionRequestTimeout() {
return connectionRequestTimeout;
}
/**
* Determines the timeout in milliseconds until a connection is established.
* A timeout value of zero is interpreted as an infinite timeout.
* A timeout value of zero is interpreted as an infinite timeout.
* A negative value is interpreted as undefined (system default).
* Default: {@code -1}
*/
public int getConnectTimeout() {
return connectTimeout;
}
/**
* Defines the socket timeout ({@code SO_TIMEOUT}) in milliseconds,
* which is the timeout for waiting for data or, put differently,
* a maximum period inactivity between two consecutive data packets).
* A timeout value of zero is interpreted as an infinite timeout.
* A negative value is interpreted as undefined (system default).
* Default: {@code -1}
*/
public int getSocketTimeout() {
return socketTimeout;
}
源码中默认的时间:
系统默认的超时时间:
1.修改maxThread=1024,提高一倍的线程数
2.修改connectionTimeout=2000,没数据了之后2秒就断,别等这么久(keepAlivetimeout是请求处理完了之后等多久关闭连接,connectionTimeout是本条连接等多久没数据关连接)
3.修改/proc/sys/net/ipv4/目录下的tcp_retries2文件为4,别重发15次了,一般也用不了这么多
其他示例:
/**
* 设置 连接超时、 请求超时 、 读取超时 毫秒
* @param requestConfig
* @return
*/
private static RequestConfig setTimeOutConfig(RequestConfig requestConfig){
return RequestConfig.copy(requestConfig)
.setConnectionRequestTimeout(60000)
.setConnectTimeout(60000)
.setSocketTimeout(10000)
.build();
}
注意:
程序中最好设置connectTimeout、socketTimeout,可以防止阻塞。
如果不设置connectTimeout会导致,建立tcp链接时,阻塞,假死。
如果不设置socketTimeout会导致,已经建立了tcp链接,在通信时,发送了请求报文,恰好此时,网络断掉,程序就阻塞,假死在那。
解决问题
所以,我修改了线程池的队列容量和最大线程。当大于核心线程数时,5个请求将进入到队列。其他线程将使用最大线程来处理。这里说明使用的线程池应该采用SynchronousQueue同步队列。通过设置队列大小为0会自动构建同步器队列。
protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
return (BlockingQueue)(queueCapacity > 0 ? new LinkedBlockingQueue(queueCapacity) : new SynchronousQueue());
}