故事背景:
由于线上需要及时的刷新某些第三方数据,导致请求第三方接口需要频繁调用。然而由于某些原因,服务器把该第三方地址给拉进黑名单了,导致频繁请求超时,线程耗尽呈一直等待状态,导致后续请求阻塞。
解决方案:
本应是由网管解决网络不通畅的问题。但是由于网管重启技术有限,使用技术手段进行尝试规避——为伪高并发接口配置线程池,限制它支配线程的自由,从而达到不影响其他网络请求性能的目的。
项目框架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类在初始化的时候有一个构造器是传入前缀名称的方式,遂用这个子类来实例化我们的线程工厂。
后感:
对我来说,可能会解决掉高并发“死机”的问题。把存在高并发风险的接口用容器隔离出来,让他不跟其他请求抢资源,从而达到服务稳定的效果。