PAYATHON 2026

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 项目也可以考虑虚拟线程。无论采用哪种方式,都要配置网络超时、线程或并发上限、异常处理和支付请求的幂等机制。

备注:内容仅供参考。