概览
为什么创建 Spring WebFlux?
部分原因是需要一个非阻塞的 Web 栈,以使用少量线程处理并发并以更少的硬件资源进行扩展。Servlet 非阻塞 I/O 使得其脱离了 Servlet API 的其余部分,其中契约是同步的(Filter、Servlet)或阻塞的(getParameter、getPart)。这是创建新的通用 API 的动机,该 API 可作为任何非阻塞运行时的基础。这很重要,因为像 Netty 这样的服务器在异步、非阻塞领域已经非常成熟。
答案的另一部分是函数式编程。就像 Java 5 中添加的注解创造了机会(例如带注解的 REST 控制器或单元测试)一样,Java 8 中添加的 Lambda 表达式也为 Java 中的函数式 API 创造了机会。这对非阻塞应用程序和延续式 API(通过 CompletableFuture 和 ReactiveX 流行起来)来说是一个福音,它们允许声明性地组合异步逻辑。在编程模型级别,Java 8 使 Spring WebFlux 能够提供函数式 Web 端点以及带注解的控制器。
定义“响应式”
我们提到了“非阻塞”和“函数式”,但“响应式”到底是什么意思?
术语“响应式”指的是围绕对变化做出反应而构建的编程模型——网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,等等。从这个意义上说,非阻塞是响应式的,因为我们不再被阻塞,而是以对操作完成或数据可用时的通知做出反应的模式。
我们 Spring 团队还将另一个重要的机制与“响应式”联系起来,那就是非阻塞背压。在同步的命令式代码中,阻塞调用作为一种自然的背压形式,强制调用者等待。在非阻塞代码中,控制事件速率变得很重要,以防止快速生产者压倒其目的地。
响应式流(Reactive Streams)是一个小规范(也在 Java 9 中采用),它定义了带有背压的异步组件之间的交互。例如,数据仓库(作为发布者)可以生成数据,然后 HTTP 服务器(作为订阅者)可以将数据写入响应。响应式流的主要目的是让订阅者控制发布者生成数据的速度。
|
常见问题:如果发布者无法减速怎么办? 响应式流的目的只是建立机制和边界。如果发布者无法减速,它必须决定是缓冲、丢弃还是失败。 |
响应式 API
响应式流对于互操作性起着重要作用。它对库和基础设施组件很有用,但作为应用程序 API 则不太有用,因为它太底层了。应用程序需要更高级别、更丰富的函数式 API 来组合异步逻辑——类似于 Java 8 的 Stream API,但不仅限于集合。这就是响应式库所扮演的角色。
Reactor 是 Spring WebFlux 首选的响应式库。它提供了 Mono 和 Flux API 类型,通过与 ReactiveX 运算符词汇表对齐的丰富运算符集来处理 0..1 (Mono) 和 0..N (Flux) 的数据序列。Reactor 是一个响应式流库,因此,它的所有运算符都支持非阻塞背压。Reactor 非常关注服务器端 Java。它与 Spring 紧密合作开发。
WebFlux 需要 Reactor 作为核心依赖,但它通过响应式流与其他响应式库互操作。通常,WebFlux API 接受纯 Publisher 作为输入,将其内部适配到 Reactor 类型,使用它,并返回 Flux 或 Mono 作为输出。因此,您可以将任何 Publisher 作为输入传递,并且可以对输出应用操作,但您需要将输出适配以用于另一个响应式库。只要可行(例如,带注解的控制器),WebFlux 就会透明地适配使用 RxJava 或另一个响应式库。有关更多详细信息,请参阅响应式库。
| 除了响应式 API,WebFlux 还可以与 Kotlin 中的协程 API 一起使用,它提供了一种更命令式的编程风格。以下 Kotlin 代码示例将与协程 API 一起提供。 |
编程模型
spring-web 模块包含 Spring WebFlux 所依赖的响应式基础,包括 HTTP 抽象、支持服务器的响应式流适配器、编解码器以及核心WebHandler API,它可与 Servlet API 媲美,但具有非阻塞契约。
在此基础上,Spring WebFlux 提供了两种编程模型选择:
适用性
Spring MVC 还是 WebFlux?
一个很自然的问题,但它建立了一个不健全的二分法。实际上,两者协同工作以扩展可用选项的范围。两者都旨在实现连续性和一致性,它们并存,并且来自每一方的反馈都使双方受益。下图显示了两者之间的关系,它们的共同点以及各自独特支持的内容:
我们建议您考虑以下具体要点:
-
如果您有一个运行良好的 Spring MVC 应用程序,则无需更改。命令式编程是编写、理解和调试代码的最简单方法。由于历史上大多数库都是阻塞的,因此您可以最大程度地选择库。
-
如果您已经在寻找非阻塞 Web 栈,Spring WebFlux 提供了与该领域中其他栈相同的执行模型优势,还提供了服务器选择(Netty、Tomcat、Jetty 和 Servlet 容器)、编程模型选择(注解控制器和函数式 Web 端点)以及响应式库选择(Reactor、RxJava 或其他)。
-
如果您对用于 Java 8 Lambda 或 Kotlin 的轻量级函数式 Web 框架感兴趣,可以使用 Spring WebFlux 函数式 Web 端点。对于需求不那么复杂的小型应用程序或微服务,这也可以是一个不错的选择,它们可以受益于更高的透明度和控制。
-
在微服务架构中,您可以混合使用 Spring MVC 或 Spring WebFlux 控制器,或者使用 Spring WebFlux 函数式端点的应用程序。在两个框架中都支持相同的基于注解的编程模型,这使得重用知识变得更容易,同时也能为正确的工作选择正确的工具。
-
评估应用程序的一种简单方法是检查其依赖项。如果您有阻塞的持久化 API(JPA、JDBC)或网络 API 要使用,至少对于常见架构而言,Spring MVC 是最佳选择。使用 Reactor 和 RxJava 在单独的线程上执行阻塞调用在技术上是可行的,但您将无法充分利用非阻塞 Web 栈。
-
如果您有一个调用远程服务的 Spring MVC 应用程序,请尝试响应式
WebClient。您可以直接从 Spring MVC 控制器方法返回响应式类型(Reactor、RxJava 或其他)。每次调用的延迟越大,或者调用之间的相互依赖性越大,收益就越显著。Spring MVC 控制器也可以调用其他响应式组件。 -
如果您的团队规模庞大,请记住转向非阻塞、函数式和声明式编程的学习曲线很陡峭。一种无需完全转换即可开始的实用方法是使用响应式
WebClient。除此之外,从小处着手并衡量收益。我们预计,对于广泛的应用程序而言,这种转换是不必要的。如果您不确定要寻找哪些收益,请从了解非阻塞 I/O 的工作原理(例如,单线程 Node.js 上的并发)及其影响开始。
服务器
Spring WebFlux 支持 Tomcat、Jetty、Servlet 容器,以及 Netty 等非 Servlet 运行时。所有服务器都适配到一个低级通用 API,以便跨服务器支持高级编程模型。
Spring WebFlux 不内置支持启动或停止服务器。然而,从 Spring 配置和WebFlux 基础设施组装应用程序并用几行代码运行它很容易。
Spring Boot 有一个 WebFlux 启动器,可以自动化这些步骤。默认情况下,该启动器使用 Netty,但通过更改 Maven 或 Gradle 依赖项可以轻松切换到 Tomcat 或 Jetty。Spring Boot 默认使用 Netty,因为它在异步、非阻塞领域更广泛使用,并且允许客户端和服务器共享资源。
Tomcat 和 Jetty 都可以与 Spring MVC 和 WebFlux 一起使用。但是,请记住,它们的使用方式非常不同。Spring MVC 依赖于 Servlet 阻塞 I/O,并允许应用程序在需要时直接使用 Servlet API。Spring WebFlux 依赖于 Servlet 非阻塞 I/O,并在低级适配器后面使用 Servlet API。它不直接暴露给应用程序使用。
| 强烈建议在 WebFlux 应用程序的上下文中不要映射 Servlet 过滤器或直接操作 Servlet API。由于上述原因,在相同的上下文中混合阻塞 I/O 和非阻塞 I/O 将导致运行时问题。 |
性能
性能有许多特性和含义。响应式和非阻塞通常不会使应用程序运行得更快。在某些情况下可以——例如,如果使用 WebClient 并行运行远程调用。然而,以非阻塞方式做事需要更多的工作,这可能会略微增加所需的处理时间。
响应式和非阻塞的关键预期好处是能够以少量固定线程和更少的内存进行扩展。这使得应用程序在负载下更具弹性,因为它们以更可预测的方式进行扩展。然而,为了观察这些好处,您需要有一些延迟(包括慢速和不可预测的网络 I/O 的混合)。这正是响应式栈开始展现其优势的地方,并且差异可能非常显著。
并发模型
Spring MVC 和 Spring WebFlux 都支持注解控制器,但在并发模型以及阻塞和线程的默认假设方面存在关键差异。
在 Spring MVC(以及一般的 Servlet 应用程序)中,假设应用程序可以阻塞当前线程(例如,用于远程调用)。因此,Servlet 容器使用一个大型线程池来吸收请求处理期间可能发生的阻塞。
在 Spring WebFlux(以及一般的非阻塞服务器)中,假设应用程序不会阻塞。因此,非阻塞服务器使用一个小型、固定大小的线程池(事件循环工作线程)来处理请求。
| “扩展”和“少量线程”听起来可能相互矛盾,但永远不阻塞当前线程(而是依赖回调)意味着您不需要额外的线程,因为没有阻塞调用需要吸收。 |
调用阻塞 API
如果确实需要使用阻塞库怎么办?Reactor 和 RxJava 都提供了 publishOn 运算符,可以在不同的线程上继续处理。这意味着有一个简单的退出机制。但是请记住,阻塞 API 与这种并发模型不兼容。
可变状态
在 Reactor 和 RxJava 中,您通过操作符声明逻辑。在运行时,形成了一个响应式管道,其中数据按顺序在不同的阶段进行处理。这样做的一个主要好处是,它使应用程序无需保护可变状态,因为该管道中的应用程序代码永远不会并发调用。
线程模型
在运行 Spring WebFlux 的服务器上,您应该期望看到哪些线程?
-
在一个“纯粹的”Spring WebFlux 服务器上(例如,没有数据访问或其他可选依赖),您可以预期一个服务器线程和几个用于请求处理的线程(通常与 CPU 核数相同)。然而,Servlet 容器可能会启动更多的线程(例如,Tomcat 上有 10 个),以支持 Servlet(阻塞)I/O 和 Servlet 3.1(非阻塞)I/O 的使用。
-
响应式
WebClient以事件循环方式运行。因此,您可以看到与之相关的少量固定处理线程(例如,使用 Reactor Netty 连接器时的reactor-http-nio-)。然而,如果 Reactor Netty 同时用于客户端和服务器,两者默认共享事件循环资源。 -
Reactor 和 RxJava 提供了线程池抽象,称为调度器,用于与
publishOn运算符一起使用,该运算符用于将处理切换到不同的线程池。调度器的名称暗示了特定的并发策略——例如,“并行”(用于 CPU 密集型工作,线程数量有限)或“弹性”(用于 I/O 密集型工作,线程数量多)。如果您看到此类线程,则表示某些代码正在使用特定的线程池Scheduler策略。 -
数据访问库和其他第三方依赖项也可以创建和使用自己的线程。