FunTester CompletableFuture 实现并发调用

FunTester · 2025年04月18日 · 663 次阅读

在现代应用开发中,并行处理就像是一群配合默契的大厨,各自忙碌却井然有序,一起炒菜、煲汤、做冷盘,不但节省了时间,还大大提高了出菜速度。系统也是一样,当我们将任务合理拆分并交由多个 “线程厨师” 同时处理时,整体吞吐量自然水涨船高,响应时间也就被大大压缩。与其让一个线程从头忙到尾,不如分头并进、各显神通。

串行调用的性能瓶颈

在传统的串行调用模式下,系统就像一个单人操作的厨房,每位厨师只能按部就班地完成一项任务。你必须等第一个菜做好,才能开始做第二道菜,甚至第三道菜也要排队。这样的操作方式有几个明显的问题:

  • 每道菜必须等待前一道菜做完才能开始,导致所有任务串行执行,效率低下;
  • 总体的做菜时间就是各道菜的时间总和,导致准备时间拖得很长;
  • 如果遇到炉火不够猛(例如网络延迟)或者某道菜特别难做(例如接口响应慢),就会拖慢整个准备过程。

而一旦网络不稳定或接口响应缓慢,问题就会更加严重,导致整个 “厨房” 瘫痪,顾客等得心急如焚,系统性能也随之下滑,用户体验直线下降。

并发调用的必要性

REST 架构就像是一个高效的多厨师厨房,每个厨师都能独立操作,不必等前一个厨师的工作完成,大家各自负责自己的任务,节省了大量时间。它的并发调用优势体现得尤为明显:

  • 不同的接口可以同时处理请求,就像多位厨师同时准备不同的菜肴,减少了总的等待时间;
  • 充分调动了每个厨师的工作能力,避免了厨房里有人闲着等待,CPU 和网络资源也得到了高效利用;
  • 这样一来,顾客就能在更短的时间内拿到餐点,提升了整体用餐体验,页面加载速度也因此变得更快。

典型应用场景就像是厨房里处理不同类型的菜肴:

  • 多种原材料的聚集,就像是从不同食材供应商那里获取的多种原料,快速准备用户信息、订单状态、库存等数据;
  • 无依赖关系的独立任务,正如每位厨师只负责自己的专长菜肴,互不影响;
  • 对时间要求较高的业务场景,类似顾客已经等得不耐烦了,厨师们必须尽快上菜,否则就会影响顾客的用餐体验。

CompletableFuture 实现并发请求

CompletableFuture 作为 Java 8 引入的异步编程工具,提供了强大的任务编排能力。以下示例展示了如何使用它实现 REST 接口的并行调用:

package com.funtester.example;

import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

public class ParallelRestCalls {

    // 创建一个 HttpClient 实例,用于发送 HTTP 请求
    private static final HttpClient client = HttpClient.newHttpClient();

    /**
     * fetch 方法:用于异步请求一个 URL 并返回响应体的内容
     * 
     * @param url 要请求的 URL
     * @return 一个 CompletableFuture,包含 HTTP 请求的响应体
     */
    public static CompletableFuture<String> fetch(String url) {
        // 构建一个 HttpRequest 对象,指定请求的 URI
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url)) // 设置请求的 URL
                .build();

        // 异步发送请求,并返回响应体的内容
        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())  // 发送异步请求
                .thenApply(HttpResponse::body); // 获取响应体内容
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 定义一个包含多个 URL 的列表,这些 URL 将用于并行发送请求
        List<String> urls = Arrays.asList(
                "https://api.funtester.com/resource1", // 第一个资源
                "https://api.funtester.com/resource2", // 第二个资源
                "https://api.funtester.com/resource3"  // 第三个资源
        );

        // 使用流操作并行地发出请求
        List<CompletableFuture<String>> futures = urls.stream()  // 将 URL 列表转换为流
                .map(ParallelRestCalls::fetch) // 对每个 URL 调用 fetch 方法
                .collect(Collectors.toList()); // 收集所有的 CompletableFuture 对象

        // 等待所有请求都完成,这里使用 CompletableFuture.allOf 来处理多个异步任务
        CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

        // 当所有请求都完成后,将所有响应收集到一个列表中
        CompletableFuture<List<String>> allResponses = allOf.thenApply(v ->
                futures.stream() // 对每个 CompletableFuture 进行操作
                        .map(CompletableFuture::join) // 获取每个请求的结果(阻塞等待)
                        .collect(Collectors.toList()) // 收集所有响应到列表中
        );

        // 获取所有响应的结果,并打印输出
        List<String> responses = allResponses.get(); // 阻塞等待并获取最终结果
        responses.forEach(System.out::println); // 输出每个响应
    }
}

这段代码演示了如何使用 CompletableFuture 实现 REST 接口的并行请求,充分利用异步编程提高性能。在发送多个 REST 请求时,这种并行处理方式显著降低了总的响应时间,尤其是在面对多个独立请求时,可以大大提升系统的吞吐量和响应速度。

异常处理机制

实际生产环境中必须考虑接口调用失败的情况。CompletableFuture 提供了多种异常处理方式:

方案一:使用 exceptionally

/**
 * 异步发送 HTTP 请求并返回响应体的内容。
 *
 * @param url 要请求的 URL 地址
 * @return 一个 CompletableFuture,包含 HTTP 请求的响应体内容,若发生异常,返回错误信息
 */
public static CompletableFuture<String> fetch(String url) {
    // 创建一个 HttpRequest 对象,构建请求并指定请求的 URL
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))  // 设置请求的 URI 地址
            .build();  // 构建 HttpRequest 对象

    // 异步发送请求,处理响应并返回响应体内容
    return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())  // 异步发送请求并获取响应体
            .thenApply(HttpResponse::body)  // 提取响应体作为结果
            .exceptionally(ex -> "Error: " + ex.getMessage());  // 如果发生异常,返回错误信息
}

方案二:使用 handle

/**
 * 异步发送 HTTP 请求并返回响应体的内容。如果发生异常,返回错误信息。
 *
 * @param url 要请求的 URL 地址
 * @return 一个 CompletableFuture,包含 HTTP 请求的响应体内容;若发生异常,返回错误信息
 */
public static CompletableFuture<String> fetch(String url) {
    // 创建一个 HttpRequest 对象,构建请求并指定请求的 URL
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))  // 设置请求的 URI 地址
            .build();  // 构建 HttpRequest 对象

    // 异步发送请求,处理响应并返回响应体内容,或者捕获异常
    return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())  // 异步发送请求并获取响应体
            .handle((response, ex) -> {  // 使用 handle 方法处理响应或异常
                if (ex != null) {  // 如果发生异常
                    return "Error: " + ex.getMessage();  // 返回错误信息
                }
                return response.body();  // 如果没有异常,返回响应体的内容
            });
}

这种方式可以确保无论请求是否成功,都会返回一个值(响应体或者错误信息)。handle 方法与 exceptionally 方法类似,区别在于它不仅能处理异常,还能处理正常响应。

技术优势总结

采用 CompletableFuture 来实现并发调用,犹如厨房里多个厨师分工合作、互不干扰,每个人专注于自己的一道菜。它的优势也正如多个厨师并行工作所带来的好处:

  • 显著提升系统吞吐量:不同的任务可以并行处理,就像多位厨师在不同的灶台上忙碌,各自分担烹饪任务,整体产出自然提高,系统吞吐量大幅增加;
  • 优化资源利用率:就像厨房里的锅灶和食材被充分利用一样,CompletableFuture 可以高效调度线程,避免 CPU 和网络空闲,确保资源得到最大化的利用;
  • 提供灵活的异常处理机制:每道菜在准备过程中可能会出问题,但只要每个厨师有预案,问题就能迅速被发现并处理。CompletableFuture 提供了 exceptionally 和 handle 方法,使得异常处理更加灵活可靠,避免了系统崩溃的风险;
  • 代码简洁易维护:并行调用的实现方式比传统的回调或多线程方式更加简洁清晰,代码结构一目了然,减少了维护的复杂度。

通过合理运用并行处理技术,开发者就能像厨房里的大厨一样,在保证系统稳定性和安全性的前提下,大幅提升接口的响应速度,最终为用户提供更加高效和优质的服务体验。

FunTester 原创精华
【免费合集】从 Java 开始性能测试
故障测试与 Web 前端
服务端功能测试
性能测试专题
Java、Groovy、Go
测试开发、自动化、白盒
测试理论、FunTester 风采
视频专题
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册