Yujun's Blog

多线程提升API性能的正确姿势

July 9, 2025 (2w ago)SD

多线程提升API性能的正确姿势

在我们的日常工作中,经常会遇到这样的场景,比如负责的某个API接口,明明每一步逻辑都不复杂,但因为它需要调用好几个其他服务或者查询好几次数据库,整个接口响应很慢。用户在抓狂,监控在报警,老板在催你优化。

今天,这篇教程将手把手带你走出这个困境。我们将通过一个极其常见的业务场景——电商商品详情页的聚合数据加载,来学习如何运用多线程技术,让的API性能飞起来。

场景分析:为什么我们的接口这么慢?

当用户打开一个商品详情页时,我们需要展示哪些信息?

  1. 商品基本信息:名称、描述、规格等。(来自商品库,耗时T1 = 30ms)
  2. 实时价格与库存:可能需要单独调用计价中心和库存中心。(来自库存服务,耗时T2 = 50ms)
  3. 营销活动信息:这个商品正在参加什么满减、拼团活动?(来自营销库,耗时T3 = 40ms)
  4. 用户评价摘要:好评率、最新几条评价。(来自评价库,耗时T4 = 25ms)

如果用最直观的串行方式编码,代码会是这样:

public ProductDetailVO getProductDetail(String productId) {
    // 步骤1: 获取商品信息
    ProductInfo info = productRpcService.getProductInfo(productId); // 耗时 30ms
    
    // 步骤2: 获取库存信息
    StockInfo stock = stockRpcService.getStock(productId); // 耗时 50ms
    
    // 步骤3: 获取营销信息
    MarketingInfo marketing = marketingDb.queryByProductId(productId); // 耗时 40ms
    
    // 步骤4: 获取用户评价
    CommentSummary comments = commentDb.querySummary(productId); // 耗时 25ms
    
    // 组装返回结果...
    return new ProductDetailVO(info, stock, marketing, comments);
}

总耗时 = 30 + 50 + 40 + 25 = 145ms。

而在互联网场景中,用户体验是生命线,接口性能更是重中之重。一个核心API的响应时间(P99),往往被严格限制在50ms-100ms以内。

这个耗时对于一个核心页面来说,是灾难性的。问题出在哪?

核心瓶颈:这四个数据查询任务互相之间没有任何依赖关系!我们在苦苦等待查询库存的时候,CPU完全可以去同时查询营销和评价信息。串行执行,就是对宝贵的响应时间的巨大浪费。

所以,一个明显的优化思路是:将这四个独立的IO密集型任务,从排队过安检模式,变为多通道同时安检模式——也就是并发执行。

解决方案:CompletableFuture

在Java中,实现并发的方式有很多,比如直接用 ThreadFutureTaskExecutorService。但在Java 8及以后,我们有了一个更强大、更易于编排的工具:CompletableFuture

CompletableFuture的优势在于其流式API和强大的组合能力,能让异步代码写得像同步代码一样清晰。

接下来我们来改造下上面的代码。

Step 1:准备一个线程池

多线程不是无成本的,频繁创建和销毁线程开销很大。在项目中,我们必须使用线程池来管理线程。

// 在配置类(Configuration)中定义一个全局的线程池Bean
@Configuration
public class ThreadPoolConfig {

    @Bean("io密集型线程池") // 给Bean起个明确的名字
    public ExecutorService ioExecutorService() {
        // 获取CPU核心数,用于计算线程数
        int coreCount = Runtime.getRuntime().availableProcessors();
        
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                coreCount,          // 核心线程数
                coreCount * 2,      // 最大线程数
                60L,                // 空闲线程存活时间
                TimeUnit.SECONDS,   // 时间单位
                new LinkedBlockingQueue<>(2000), // 任务队列
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );
        return executor;
    }
}

面试小贴士:为什么核心线程数设为coreCount,最大设为coreCount * 2?这是IO密集型任务的常见配置。因为IO任务大部分时间在等待(比如等数据库返回),CPU是空闲的,所以可以设置比CPU核心数更多的线程来提高吞吐。

Step 2:使用 CompletableFuture.supplyAsync 提交并行任务

现在,我们来重写 getProductDetail方法。

@Service
public class ProductDetailService {

    @Resource(name = "io密集型线程池") // 注入我们定义的线程池
    private ExecutorService executorService;
    
    // ... 注入各种RPC和DAO服务

    public ProductDetailVO getProductDetail(String productId) throws ExecutionException, InterruptedException {
        // 1. 提交所有异步任务到线程池
        CompletableFuture<ProductInfo> infoFuture = CompletableFuture.supplyAsync(() -> {
            // 任务1:获取商品信息
            return productRpcService.getProductInfo(productId);
        }, executorService);

        CompletableFuture<StockInfo> stockFuture = CompletableFuture.supplyAsync(() -> {
            // 任务2:获取库存信息
            return stockRpcService.getStock(productId);
        }, executorService);

        CompletableFuture<MarketingInfo> marketingFuture = CompletableFuture.supplyAsync(() -> {
            // 任务3:获取营销信息
            return marketingDb.queryByProductId(productId);
        }, executorService);
        
        CompletableFuture<CommentSummary> commentsFuture = CompletableFuture.supplyAsync(() -> {
            // 任务4:获取用户评价
            return commentDb.querySummary(productId);
        }, executorService);

        // 2. 等待所有任务完成
        // CompletableFuture.allOf() 接收一个CompletableFuture数组,当所有任务都完成时,它才会完成。
        CompletableFuture.allOf(infoFuture, stockFuture, marketingFuture, commentsFuture).join();

        // 3. 从Future中获取结果并组装
        ProductInfo info = infoFuture.get();
        StockInfo stock = stockFuture.get();
        MarketingInfo marketing = marketingFuture.get();
        CommentSummary comments = commentsFuture.get();
        
        return new ProductDetailVO(info, stock, marketing, comments);
    }
}

通过改造,四个任务被同时提交到了线程池中并行执行。此时,整个数据准备阶段的总耗时不再是累加,而是取决于最慢的那个任务

总耗时 ≈ max(30, 50, 40, 25) = 50ms。

接口性能从 145ms 优化到了 50ms,提升了近 65%,这是一个巨大的飞跃。

进阶与最佳实践

上面的代码已经能很好地工作了,但在真实的企业级项目中,我们还需要考虑更多细节。

  1. 优雅的异常处理

如果其中一个异步任务失败了怎么办?get() 方法会抛出 ExecutionException。我们可以用 exceptionally来处理。

CompletableFuture<ProductInfo> infoFuture = CompletableFuture.supplyAsync(...)
        .exceptionally(ex -> {
            // 如果获取商品信息失败,可以返回一个默认的、空的ProductInfo对象
            log.error("获取商品信息失败", ex);
            return new ProductInfo(); // 服务降级
        });
  1. 超时控制

不能让一个慢服务拖垮整个接口。可以结合 orTimeout (Java 9+) 或 completeOnTimeout (Java 9+) 来设置超时。

CompletableFuture<StockInfo> stockFuture = CompletableFuture.supplyAsync(...)
        .orTimeout(60, TimeUnit.MILLISECONDS); // 设置60毫秒超时

如果 stockRpcService 超过60ms还没返回,stockFuture.get() 会抛出 TimeoutException,我们可以捕获它并进行降级处理。

  1. 结果组合的更优写法

allOf(...).join() 之后再挨个 .get() 有点繁琐。我们可以利用 thenCombine 或 thenApply 等方法,以更函数式的方式组合结果。但对于初学者,allOf + get 的方式更直观易懂。

思考:何时应该使用多线程?

多线程是把双刃剑,它能提升性能,但也会增加代码复杂度和资源消耗。请记住这个黄金法则:

当业务流程中包含多个【互相独立】的【IO密集型】任务时,就是使用多线程进行并优化的最佳时机。

  • 互相独立:任务A的执行不依赖任务B的结果。
  • IO密集型:任务的大部分时间都在等待网络或磁盘响应,比如数据库查询、RPC调用、文件读写。

如果任务是CPU密集型(如复杂的数学计算),或者任务之间有强依赖关系(必须先拿到A的结果才能执行B),那么使用多线程可能无法带来性能提升,甚至会因为线程切换的开销而变慢。

核心要点回顾:

  • 识别瓶颈:分析业务流程,找出那些可以并行的、独立的IO任务。
  • 创建线程池:在项目中全局管理线程,避免手动创建。
  • 提交任务:使用 CompletableFuture.supplyAsync 将任务提交到线程池。
  • 等待并聚合:使用 CompletableFuture.allOf(...).join() 等待所有任务完成。
  • 获取结果:从 Future 对象中获取各个任务的执行结果并组装。
  • 考虑健壮性:做好异常处理和超时控制,保证系统的稳定性。

Comments