记一次由于多次请求超时导致伪高并发,使得Tomcat线程耗尽响应速度慢的线上问题

故事背景:
由于线上需要及时的刷新某些第三方数据,导致请求第三方接口需要频繁调用。然而由于某些原因,服务器把该第三方地址给拉进黑名单了,导致频繁请求超时,线程耗尽呈一直等待状态,导致后续请求阻塞。
解决方案:
本应是由网管解决网络不通畅的问题。但是由于网管重启技术有限,使用技术手段进行尝试规避——为伪高并发接口配置线程池,限制它支配线程的自由,从而达到不影响其他网络请求性能的目的。
项目框架springboot+threadPoolExecutor。
springboot的优势莫过于万物皆可配,最常用的方式则是新建一个类,取名为XXXConfig,这也是我的编码习惯。如下:
在这里插入图片描述
使用线程池的方式有很多,我这里介绍我所使用的方式:
使用线程池的方式,把高并发的请求丢到线程池中,由线程池中的线程、工作队列、拒绝策略进行分配请求的处理。可以理解成线程池向系统资源申请了一块区域,而这些请求仅在这块区域中进行活动,所以对于性能的消耗在这块隔离出来,保证了其他模块的稳定性。
谈到线程池,就不得不提线程池的几大参数:

int corePoolSize //线程池核心线程数量,可以理解成最小允许线程数量
int maximumPoolSize //线程池最大线程数量
long keepAliveTime //线程保持存活时间长度
TimeUnit unit //线程保持存活时间单位(微秒,秒,分,时,天等)
BlockingQueue<Runnable> workQueue //单条线程的工作队列,只有当条线程队列满了之后,才会去检查是否能开启一条新线程进行处理请求
ThreadFactory threadFactory //线程工厂,可以初始化线程池中的线程。在里面定义自己的线程名称或者前缀名称还是挺好用的。也可以重写Thread的run方法
RejectedExecutionHandler handler //拒绝策略

已上对线程池有所了解之后,开始实际操作。

首先

上面提到了把请求丢进线程池中异步调用,则需要开启springboot的允许异步请求,在启动类上添加注解即可
@EnableAsync

然后

开始写我们的线程池配置类如下MyThreadPoolExecutorConfig :
注册Bean,我这里是自定义命名为myThreadPoolExecutor

package com.mySpring.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;

import java.util.concurrent.*;

@Configuration
public class MyThreadPoolExecutorConfig {

    private Logger logger = LoggerFactory.getLogger(MyThreadPoolExecutorConfig.class);

    @Bean(name = "myThreadPoolExecutor")
    public Executor myThreadPoolExecutor() {
        logger.info("开启我的线程池执行器配置!");

        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(5, 5,
                        60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1),
                        new ThreadPoolExecutor.CallerRunsPolicy());
        ThreadFactory threadFactory = new CustomizableThreadFactory("myThread-async-");
        threadPoolExecutor.setThreadFactory(threadFactory);
        logger.info("我的线程池配置完毕!");

        return threadPoolExecutor;

    }
}

接着

编写我们的Controller/Service(接口访问)
在我们需要线程池帮助的高并发接口上添加注解
@Async(value = “myThreadPoolExecutor”)即可

 @GetMapping(value = "/getTestThread")
    @Async(value = "myThreadPoolExecutor")
    public test getTestThread() throws InterruptedException {
        System.out.println("异步执行中,当前线程为:"+Thread.currentThread().getName());
        Thread.sleep(10*1000);
        return new test();
    }

最后测试

@Test
    void testThreadPool() throws InterruptedException {
        int i=30;
        while(i>0)
        {
            testController.getTestThread();
            i--;
        }

        System.out.println("执行结束!");
    }

总结

我这里使用的线程池是通过ThreadPoolExecutor这个类进行初始化的,大家可以对比一下他相较于ThreadExecutor初始化的好处。(前者可以所有参数自定义,更加的根据实际生产环境进行可用度更高的配置

我这里是初始化最大线程数为5的线程池,并且我自定义了我的线程名称,我是给Thread加了个前缀名myThread-async-
给大家看下实际运行效果,如下图:
在这里插入图片描述
可以看到线程名称的取名方式是:自定义的前缀+序号。并且发现序号最大是5。
我这里调用了30次接口(注意,要模拟高并发的场景,即需要一直有消息处于待处理状态,而不能下一个请求进来时,上一个请求已经处理完毕了)。理论上来说,这30次接口会分配给5个不同的线程进行执行,并且只有5条线程,实际也确实如此。至此,我们的线程池使用是成功的!

开发过程中遇到的问题分享:

当然事情不可能这么顺利,但是问题都是可以解决的。
1、一开始我只在方法上注解了@Async,结果运行的时候打印的线程名称是Simple开头的,而不是我自定义的,说明线程池并没有生效。
解决: 通过阅读控制台日志,发现系统报了一个找不到自定义线程池配置的一个日志,然后还打了一个找不到"taskExecutor"的线程池日志。可以想象springboot默认的一个线程池名称则是taskExecutor ,由于我没有给Async指定我自定义的线程名称,导致他去搜索默认名称的Bean也没有找到。所以我给Async添加了一个value值。 那么举一反三,想当然的就可以如果有多个线程池的话,只要命名规范,然后在相应方法上给出正确的线程池名称即可。
2、我看网上的教程千篇一律,可能第一个吃螃蟹的人使用了ThreadPoolTaskExecutor进行自定义线程池,后续的人都是这样写吧。这个方法是直接有命名线程前缀名的。而我的线程工厂模式并没有。
解决: 通过查找ThreadFactory的子类,筛查符合前缀初始化的工厂子类发现,CustomizableThreadFactory类在初始化的时候有一个构造器是传入前缀名称的方式,遂用这个子类来实例化我们的线程工厂。

后感:
对我来说,可能会解决掉高并发“死机”的问题。把存在高并发风险的接口用容器隔离出来,让他不跟其他请求抢资源,从而达到服务稳定的效果。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值