Java 中如何并发调用两个 webservice 提升性能
结论
这两个 WebService 调用彼此独立,可以同时发起,最后再统一等待结果。Java 8 及以上版本可以使用 CompletableFuture,并为阻塞式 HTTP 调用配置专用线程池。
改为并发执行后,总耗时通常取决于较慢的调用:
总耗时 ≈ max(paymentGateway耗时, customerDetails耗时)
如果两个调用都达到各自的超时时间,理论总耗时可以从约 40 秒缩短到约 30 秒。30 秒只是超时上限,并不表示每次调用都会耗时 30 秒。
使用 CompletableFuture 并发调用
假设两个 WebService 客户端都是同步阻塞式客户端,可以把它们放进两个异步任务:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PaymentService {
private final CustomerClient customerClient;
private final PaymentGatewayClient paymentGatewayClient;
private final ExecutorService webServiceExecutor =
Executors.newFixedThreadPool(20);
public PaymentService(
CustomerClient customerClient,
PaymentGatewayClient paymentGatewayClient) {
this.customerClient = customerClient;
this.paymentGatewayClient = paymentGatewayClient;
}
public PaymentResult processPayment(PaymentRequest request) {
CompletableFuture<CustomerDetails> customerFuture =
CompletableFuture.supplyAsync(
() -> customerClient.getCustomerDetails(
request.getCustomerId()),
webServiceExecutor
);
CompletableFuture<GatewayResult> paymentFuture =
CompletableFuture.supplyAsync(
() -> paymentGatewayClient.processPayment(request),
webServiceExecutor
);
CompletableFuture.allOf(customerFuture, paymentFuture).join();
CustomerDetails customerDetails = customerFuture.join();
GatewayResult gatewayResult = paymentFuture.join();
return new PaymentResult(customerDetails, gatewayResult);
}
}
两个 supplyAsync 会立即提交任务。程序运行到 allOf(...).join() 时,当前线程才开始等待,此时两个远程请求已经在不同线程中并发执行。
CompletableFuture.allOf() 只负责等待所有任务完成,不会直接返回各个任务的结果。等待结束后,仍需从对应的 Future 中取出结果。
不要用公共线程池承载大量阻塞请求
如果不传入 Executor,CompletableFuture.supplyAsync() 默认使用 ForkJoinPool.commonPool():
CompletableFuture.supplyAsync(() -> callWebService());
这种写法很简洁,但远程 HTTP 调用属于阻塞 I/O。请求量较大时,这些调用会长时间占用公共线程池,可能影响应用里的其他异步任务。因此,阻塞调用更适合放在独立且有容量上限的线程池中。
线程池大小也不能因为每次只有两个调用就固定设为 2,还要考虑服务本身的并发请求量。比如同时处理 100 个支付请求,最多可能产生 200 个外部调用。线程池、HTTP 连接池和下游服务的承载能力需要放在一起评估。
在 Spring 应用中,可以把线程池声明为由容器管理的 Bean,方便统一配置,并在应用关闭时释放资源。
设置整体等待超时
WebService 客户端自身的 10 秒和 30 秒超时仍需保留,同时还可以为整个异步流程设置等待上限。
Java 9 及以上版本可以使用 orTimeout:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
CompletableFuture<CustomerDetails> customerFuture =
CompletableFuture.supplyAsync(
() -> customerClient.getCustomerDetails(request.getCustomerId()),
webServiceExecutor
);
CompletableFuture<GatewayResult> paymentFuture =
CompletableFuture.supplyAsync(
() -> paymentGatewayClient.processPayment(request),
webServiceExecutor
);
CompletableFuture<Void> combinedFuture =
CompletableFuture.allOf(customerFuture, paymentFuture)
.orTimeout(31, TimeUnit.SECONDS);
combinedFuture.join();
CustomerDetails customerDetails = customerFuture.join();
GatewayResult gatewayResult = paymentFuture.join();
整体等待时间可以比最长的 HTTP 调用超时稍长一些,给连接建立、线程调度和结果处理留出少量时间。具体数值要根据客户端的实际行为和服务 SLA 决定,并非一定要设为 31 秒。
需要注意的是,orTimeout() 主要用于让 Future 以超时异常结束,不能保证底层 HTTP 请求一定会被中断。要真正限制网络调用时长,仍需配置 HTTP 客户端的连接超时、读取超时和请求超时。
正确处理异常
join() 遇到异常时会抛出未经检查的 CompletionException。生产代码需要明确区分客户信息查询失败、支付网关失败和整体等待超时:
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeoutException;
public PaymentResult processPayment(PaymentRequest request) {
CompletableFuture<CustomerDetails> customerFuture =
CompletableFuture.supplyAsync(
() -> customerClient.getCustomerDetails(
request.getCustomerId()),
webServiceExecutor
);
CompletableFuture<GatewayResult> paymentFuture =
CompletableFuture.supplyAsync(
() -> paymentGatewayClient.processPayment(request),
webServiceExecutor
);
try {
CompletableFuture.allOf(customerFuture, paymentFuture)
.orTimeout(31, TimeUnit.SECONDS)
.join();
return new PaymentResult(
customerFuture.join(),
paymentFuture.join()
);
} catch (CompletionException exception) {
Throwable cause = exception.getCause();
if (cause instanceof TimeoutException) {
throw new PaymentProcessingException(
"Timed out while waiting for external services", cause);
}
throw new PaymentProcessingException(
"External service call failed", cause);
}
}
如果业务允许在某个调用失败后使用默认结果,可以单独处理对应的 Future:
CompletableFuture<CustomerDetails> customerFuture =
CompletableFuture.supplyAsync(
() -> customerClient.getCustomerDetails(request.getCustomerId()),
webServiceExecutor
).exceptionally(exception -> CustomerDetails.unknown());
能否降级要由业务规则决定。支付结果通常不适合随意使用默认值,客户展示信息则可能允许降级。
需要注意的业务风险
两个请求在技术上互不依赖,不代表它们在业务上也适合并发执行。
如果客户信息调用负责以下任一事项:
- 验证客户是否存在;
- 检查账户状态;
- 判断是否允许支付;
- 获取风控或限额信息;
那么,在验证结束前向支付网关提交请求,可能导致不应发生的扣款。这说明两个调用在业务上实际存在依赖,不能只为缩短延迟就并发执行。
支付网关调用通常还有副作用。本地超时不代表网关没有完成扣款,直接重试可能造成重复支付。因此,应使用支付单号或幂等键,并提供支付状态查询或对账机制。
Java 21 的另一种写法
如果应用使用 Java 21,也可以采用虚拟线程。对于同步阻塞式 HTTP 客户端,这通常比维护大量平台线程更直接:
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public PaymentResult processPayment(PaymentRequest request) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<CustomerDetails> customerFuture = executor.submit(
() -> customerClient.getCustomerDetails(
request.getCustomerId())
);
Future<GatewayResult> paymentFuture = executor.submit(
() -> paymentGatewayClient.processPayment(request)
);
CustomerDetails customerDetails = customerFuture.get();
GatewayResult gatewayResult = paymentFuture.get();
return new PaymentResult(customerDetails, gatewayResult);
}
}
虚拟线程可以减少阻塞调用占用平台线程的成本,但不会加快下游 WebService,也不能替代 HTTP 连接池、超时、限流和幂等控制。
对于 Java 8 至 Java 17 项目,专用线程池配合 CompletableFuture 的兼容性较好。Java 21 项目也可以考虑虚拟线程。无论采用哪种方式,都要配置网络超时、线程或并发上限、异常处理和支付请求的幂等机制。
备注:内容仅供参考。
