@Async注解线上问题分析

Async注解使用

  1. Async注解使用时需要配合@EnableAsync注解。
  2. 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());
    }

猜你喜欢

转载自blog.csdn.net/gonghaiyu/article/details/108282359