使用CompletableFuture处理多用户API调用耗时异常问题及优化咨询
需求说明
需为100个用户调用2个API:
- 第一个OAuth令牌生成API耗时2秒
- 第二个业务API耗时8-9秒
单用户总耗时约10秒
实现与实际结果
采用CompletableFuture编写并行执行代码,预期100用户总耗时10-15秒,但实际结果不符:
- 10用户总耗时11秒
- 20用户总耗时25秒
- 100用户总耗时约100秒(单用户耗时仍为10秒)
疑问
- 为何并行执行下总耗时随用户量增加大幅上升?
- 如何优化以实现100用户总耗时低于30秒?
现有代码实现
Executor executor = Executors.newFixedThreadPool(100); List<CompletableFuture<ListA>> futures = userlist.stream() .map(req -> CompletableFuture.supplyAsync( () -> methodToCallBothApis(req.getUserId(), executor), executor )) .collect(Collectors.toList()); List<ListC> outputList = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList());
(注:原代码存在拼写错误,已修正CompleteableFutures为CompletableFuture、listA为ListA、reg为req)
耗时飙升原因
单用户内部串行+线程池误用
你的代码将线程池传入methodToCallBothApis,大概率这个方法内部是同步串行调用两个API,每个用户的任务本身就占10秒的串行时间。同时线程池设为100,看似全量并行,但API服务端普遍有并发请求限制——当请求量超过服务端处理阈值,后续请求会被排队等待,最终总耗时逼近「单用户耗时×用户数」,和你看到的100秒结果完全匹配。服务端并发瓶颈
OAuth和业务API的服务端可能仅允许同时处理10个左右的请求,100个请求会被分成10批依次处理,每批耗时10秒,总耗时自然拉到100秒。
优化方案
1. 拆分单用户API调用,实现内部并行
把单用户的两个API调用拆成并行执行,替代原有的串行逻辑:
// 线程池大小建议根据API服务端并发能力调整,比如20-30 Executor executor = Executors.newFixedThreadPool(25); List<CompletableFuture<ListC>> futures = userlist.stream() .map(req -> { // 并行发起OAuth令牌和业务API请求 CompletableFuture<String> tokenFuture = CompletableFuture.supplyAsync( () -> callOAuthApi(req.getUserId()), executor ); CompletableFuture<ListB> businessFuture = CompletableFuture.supplyAsync( () -> callBusinessApi(req.getUserId()), executor ); // 等待两个请求返回后,合并结果生成ListC return tokenFuture.thenCombine(businessFuture, (token, businessData) -> { return mergeUserResults(token, businessData); }); }) .collect(Collectors.toList()); List<ListC> outputList = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList());
调整后单用户的总耗时从10秒降到约9秒(取两个API中耗时较长的一个),同时线程池可并行处理多个用户请求。
2. 合理设置线程池大小
线程池不是越大越好:过大的线程池会增加线程上下文切换开销,还容易触发服务端限流。建议根据API服务端的并发限制设置,比如服务端允许20个并发请求,线程池设为20-25即可。
3. 复用OAuth令牌(若业务允许)
如果OAuth令牌支持复用(比如同一应用下的令牌通用、或令牌有效期内可重复使用),无需为每个用户单独调用令牌API——只需要调用1次拿到令牌,后续所有业务API请求复用该令牌,直接节省100次×2秒的耗时,总耗时能压缩到10秒以内。
4. 替换为异步HTTP客户端
如果当前用的是同步HTTP客户端(如HttpURLConnection),换成异步客户端(如OkHttp异步调用、AsyncHttpClient),线程不会阻塞在IO等待上,线程池利用率会大幅提升,能支撑更多并发请求。
优化后预期效果
- 若令牌可复用:总耗时约为业务API的最大耗时(9秒)加少量调度开销,可控制在15秒内;
- 若必须单用户拿令牌:25个线程并行处理100用户,总耗时约(100/25)×9=36秒,调整线程池到35,总耗时可降到26秒左右,满足低于30秒的要求。
内容的提问来源于stack exchange,提问作者suku

