本文介绍如何在 spring 应用中安全、高效地并行发起多个 rest 调用,解决因串行请求导致的接口响应延迟问题,并提供线程安全的响应聚合方案。
在构建基于 Spring 的微服务或网关类应用时,常遇到“父请求携带多个子请求”的典型场景:例如一个订单查询接口需同时调用库存、价格、用户画像、物流等下游服务。若对这 10–50 个子请求采用串行 HTTP 调用(如 RestTemplate 或 WebClient 同步阻塞调用),总耗时 ≈ 各子请求 RTT 之和,极易导致父接口 P95 延迟飙升、线程阻塞甚至雪崩。
根本解法是并行化 + 异步非阻塞。但直接使用 parallelStream().forEach() 会触发两个关键陷阱:
✅ 正确做法是:选用线程安全容器 + 显式标识映射关系。根据子请求是否具备唯一 ID(数字索引或字符串键),选择以下任一方案:
适用于子请求按顺序生成、ID 可作为数组下标的情形:
// 使用 synchronizedList 包装 ArrayList,保证 add(index, e) 线程安全 final ListchildResponses = Collections.synchronizedList(new ArrayList<>(Collections.nCopies(childRequests.size(), null))); childRequests.parallelStream() .forEach(request -> { ChildResponse response = callDownstreamApi(request); // 如 WebClient.post().retrieve().bodyToMono(...).block() childResponses.set(request.getId(), response); // 注意:setId() 必须返回 0~size-1 的有效索引 }); // 后续按原始顺序组装父响应(顺序敏感场景必需) ParentResponse parentResponse = buildParentResponse(childRequests, childResponses);
更通用、推荐的方式,天然支持无序响应与灵活关联:
// ConcurrentHashMap 线程安全,高性能,支持高并发 put final MapresponseMap = new ConcurrentHashMap<>() ; childRequests.parallelStream() .forEach(request -> { ChildResponse response = callDownstreamApi(request); responseMap.put(request.getRefId(), response); // 如 request.getRefId() 返回 "SKU-1001" }); // 组装时仍按 childRequests 原始顺序遍历,确保响应顺序与请求一致 List
orderedResponses = childRequests.stream() .map(req -> responseMap.get(req.getRefId())) .collect(Collectors.toList()); ParentResponse parentResponse = buildParentResponse(orderedResponses);
✨ 进阶建议:在 Spring WebFlux 环境中,应优先采用响应式编程模型:Flux responses = Flux.fromIterable(childRequests) .flatMap(req -> webClient.post() .uri("/api/child") .bodyValue(req) .retrieve() .bodyToMono(ChildResponse.class) .timeout(Duration.ofSeconds(3)) .onErrorResume(e -> Mono.just(ChildResponse.empty(req.getId()))), 10 // 并发度控制,防压垮下游 ); Mono result = responses.collectList() .map(responsesList -> buildParentResponse(responsesList));
通过合理选择线程安全容器、明确请求-响应映射关系,并结合响应式编程演进,即可在保障数据一致性的同时,将数十个子请求的总体耗时从秒级降至单次最长子请求耗时(即“木桶最短板”),显著提升 API 性能与用户体验。