多线程环境下使用Spring WebClient的正确方法

Posted

技术标签:

【中文标题】多线程环境下使用Spring WebClient的正确方法【英文标题】:Right way to use Spring WebClient in multi-thread environment 【发布时间】:2018-08-12 05:12:54 【问题描述】:

我有一个关于 Spring WebClient

的问题

在我的应用程序中,我需要执行许多类似的 API 调用,有时我需要更改调用中的标头(身份验证令牌)。那么问题来了,这两种选择哪个更好:

    为 MyService.class 的所有传入请求创建一个 WebClient,方法是将其设为 private final 字段,如下面的代码:

    private final WebClient webClient = WebClient.builder()
            .baseUrl("@987654321@")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .build();
    

这里出现了另一个问题:WebClient 是线程安全的吗? (因为服务被很多线程使用)

    为每个传入服务类的新请求创建新的 WebClient。

我想提供最大的性能,并以正确的方式使用它,但我不知道 WebClient 在其中是如何工作的,以及它期望如何使用。

谢谢。

【问题讨论】:

“所有传入请求的WebClient” --- 你的意思是“传出”请求吗? 【参考方案1】:

根据我的经验,如果您在无法控制的服务器上调用外部 API,则根本不要使用 WebClient,或者在关闭池机制的情况下使用它。连接池带来的任何性能提升都被内置于(默认 reactor-netty)库中的假设所抵消,当另一个 API 调用被远程主机突然终止时,会导致一个 API 调用出现随机错误,等等。在某些情况下,你不需要甚至不知道错误发生在哪里,因为调用都是从共享工作线程进行的。

我犯了使用 WebClient 的错误,因为 RestTemplate 的文档说它将来会被弃用。事后看来,我会使用常规的 HttpClient 或 Apache Commons HttpClient,但如果您像我一样已经使用 WebClient 实现,您可以通过如下创建 WebClient 来关闭池:

private WebClient createWebClient(int timeout) 
    TcpClient tcpClient = TcpClient.newConnection();
    HttpClient httpClient = HttpClient.from(tcpClient)
        .tcpConfiguration(client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout * 1000)
            .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(timeout))));

    return WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();


*** 创建一个单独的 WebClient 并不意味着 WebClient 会有一个单独的连接池。只需查看 HttpClient.create 的代码 - 它调用 HttpResources.get() 来获取全局资源。您可以手动提供池设置,但考虑到即使使用默认设置也会发生错误,我认为不值得冒险。

【讨论】:

正确,如 DOC 中所述,Httpclient.create() 提供了一个池化资源。但我无法理解为什么共享池会导致错误。当然,如果您订阅了相应的通量流,那么通量的工作就是处理错误,对吧?为什么会要求人们不要使用?使并发的非阻塞多个请求实际上很棒。无论如何,每个连接池的默认请求数为 500。 随机错误不会像那样发生。通量流未正确处理或您的代码存在问题。除非具体说明是什么原因造成的,否则这个答案可能会产生误导。 @rougou @Deekshith Anand 是的,我应该澄清一下我们正在使用阻塞模式,因此根本不处理我们代码中的通量流。提出问题的方式让我假设 OP 也只是想从多个请求线程进行阻塞调用。在这种情况下,您获得的唯一好处是连接池,但如果目标服务器不希望您将连接池化,您最终可能会出现随机错误,这就是我们所看到的。 在非阻塞应用程序中,我同意 WebClient 是首选,并且我在不同的项目中使用它。【参考方案2】:

关于WebClient的两个关键点:

    其 HTTP 资源(连接、缓存等)由底层库管理,由 ClientHttpConnector 引用,您可以在 WebClient 上进行配置 WebClient 是不可变的

考虑到这一点,您应该尝试在您的应用程序中重用相同的ClientHttpConnector,因为这将共享连接池——这可以说是对性能最重要的事情。这意味着您应该尝试从同一个 WebClient.create() 调用中派生所有 WebClient 实例。 Spring Boot 通过为您创建和配置一个 WebClient.Builder bean 来帮助您解决这个问题,您可以在应用程序的任何位置注入它。

因为WebClient 是不可变的,所以它是线程安全的。 WebClient 旨在用于响应式环境,其中没有任何东西与特定线程绑定(这并不意味着您不能在传统的 Servlet 应用程序中使用)。

如果您想改变发出请求的方式,有几种方法可以实现:

在构建阶段配置东西

WebClient baseClient = WebClient.create().baseUrl("https://example.org");

根据每个请求配置内容

Mono<ClientResponse> response = baseClient.get().uri("/resource")
                .header("token", "secret").exchange();

从现有的客户端实例中创建一个新的客户端实例

// mutate() will *copy* the builder state and create a new one out of it
WebClient authClient = baseClient.mutate()
                .defaultHeaders(headers -> headers.add("token", "secret");)
                .build();

【讨论】:

感谢您的完整解释。您还可以提供文档上的链接吗? (通常在文档中您会找到使用示例,但我想阅读更多关于引擎盖部分的信息。) docs.spring.io/spring-framework/docs/current/… 为什么要在整个应用程序中重复使用一个 Web 客户端?您谈到了连接池,这就是我不分享它的原因 - 假设您的应用程序依赖于您与 Web 客户端连接的两个远程服务。如果一项服务将关闭并且您没有断路器或阻止您调用死服务的东西,那么您可以使用所有连接来连接到死服务并且整个应用程序都死了,因为您没有将它分开。 我从来没有说过你应该为整个应用程序只有一个网络客户端。我相信 Reactor Netty 的连接池足够聪明,并且可以在每个主机的基础上管理连接,所以我不明白这是怎么发生的。与此同时,Spring Framework 中我们管理 HTTP 资源的方式发生了变化——所以请提出一个新问题,并随时向我指出。 多线程环境中的 WebClient 正在覆盖我的 URI。 WebClient 在类级别以下列方式初始化 private WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector((HttpClientOptions.Builder builder) -> builder.disablePool())).build();必须根据请求级别对其进行变异。让我知道这是否是正确的方法。

以上是关于多线程环境下使用Spring WebClient的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

C#多线程环境下调用 HttpWebRequest 并发连接限制

多线程环境下调用 HttpWebRequest 并发连接限制

Spring5之WebClient简单使用

Spring WebClient 使用简介

Spring WebClient 使用简介

Spring WebClient 使用简介