You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring RestTemplate结合CompletableFuture时HTTP调用阻塞问题排查

CompletableFuture并行调用RestTemplate后出现串行/等待的问题

问题背景

开发了一个REST端点,通过Spring RestTemplate调用三个独立的REST服务(使用同一主键查询数据)。为提升效率,采用CompletableFuture结合自定义线程池并行发起API调用,最后通过get()等待所有响应返回。但日志显示:RestTemplate的exchange方法执行前,三个任务处于并行状态;exchange执行后,线程变为串行执行或出现等待其他线程完成的情况。

核心代码

CompletableFuture<TypeA> callA = CompletableFuture
        .supplyAsync(() -> getCallA(id), traceableExecutorService);
CompletableFuture<TypeB> callB = CompletableFuture
        .supplyAsync(() -> getCallB(id), traceableExecutorService);
CompletableFuture<TypeC> callC = CompletableFuture
        .supplyAsync(() -> getCallC(id), traceableExecutorService);

try {
    TypeA = callA.get();
    TypeB = callB.get();
    TypeC = callC.get();
} catch (InterruptedException | ExecutionException e) {
    throw e;
}
// 组装响应并返回...

RestTemplate配置

@Bean
public RestTemplate restTemplate() {
    HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
    // 重试配置:最多重试3次,重试所有方法
    httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true));
    // 请求超时配置
    httpClientBuilder.setDefaultRequestConfig(RequestConfig.custom()
            .setConnectionRequestTimeout(10000)
            .setSocketTimeout(10000).build());
    // 连接池配置
    httpClientBuilder.setMaxConnTotal(2000);
    httpClientBuilder.setMaxConnPerRoute(100);
    
    HttpClient httpClient = httpClientBuilder.build();
    HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
    clientHttpRequestFactory.setConnectTimeout(10000);
    clientHttpRequestFactory.setReadTimeout(10000);

    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setRequestFactory(clientHttpRequestFactory);
    restTemplate.setInterceptors(Collections.singletonList(new RequestResponseLoggingInterceptor()));
    return restTemplate;
}

线程池配置

new TraceableExecutorService(factory, Executors.newWorkStealingPool(20000))

可能原因及解决思路

1. 线程池选型不当

Executors.newWorkStealingPool(20000)创建的是ForkJoinPool,参数表示并行级别而非固定线程数。ForkJoinPool的工作窃取调度更适合CPU密集型任务,对于HTTP调用这类IO密集型任务,线程会频繁因IO阻塞挂起,工作窃取机制无法充分利用线程资源,可能导致任务排队串行。

解决方案:换成适配IO密集型场景的固定线程池:

new TraceableExecutorService(factory, new ThreadPoolExecutor(
    10, // 核心线程数,可根据并发量调整,建议2*CPU核心数以上
    50, // 最大线程数
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),
    new ThreadFactoryBuilder().setNamePrefix("rest-call-worker-").build()
))

2. CompletableFuture等待顺序导致的感知串行

当前使用callA.get(); callB.get(); callC.get();的方式,会先等待callA完成,再去获取callB的结果,即使callB已经先执行完成,也会等到callA结束后才处理。这种等待顺序会让日志看起来像是串行,但实际请求是并行发起的。

解决方案:使用CompletableFuture.allOf()统一等待所有任务完成,再获取结果:

try {
    // 等待所有任务完成
    CompletableFuture.allOf(callA, callB, callC).join();
    // 此时所有任务已完成,直接获取结果
    TypeA = callA.get();
    TypeB = callB.get();
    TypeC = callC.get();
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException("并行调用外部服务失败", e);
}

3. HttpClient连接池或重试机制瓶颈

虽然当前连接池配置(总连接2000,单路由100)足够支撑三个并行请求,但如果目标服务是同一域名,且出现连接耗尽的情况(比如之前的请求未及时释放连接),会导致后续请求等待连接。另外,3次重试机制如果遇到服务响应慢,会占用线程和连接资源,加剧阻塞。

解决方案:

  • 检查连接是否正常释放:RestTemplate配合HttpComponentsClientHttpRequestFactory会自动释放连接,但若出现异常重试,需确认重试逻辑是否合理
  • 调整重试策略:只对特定可重试异常(如IO异常、连接超时)重试,减少不必要的重试占用:
    httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true, Arrays.asList(
        IOException.class, SocketTimeoutException.class
    )));
    

4. 日志观测的误区

日志中看到的“串行”可能是打印时机导致的错觉:比如在getCallX方法开头打印线程ID,之后调用RestTemplate进入IO阻塞,当第一个请求的IO返回后,线程继续执行并打印结束日志,此时其他请求可能还在IO阻塞,导致日志顺序看起来像是串行。

解决方案:在每个getCallX方法的开始和结束都打印线程ID和时间戳,确认任务的实际执行时段是否重叠。


内容的提问来源于stack exchange,提问作者Srinivasa Moorthi V

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 12:55:15