响应式编程笔记 第一部分:响应式概览

工程 | Dave Syer | 2016 年 6 月 7 日 | ...

响应式编程(又)变得有趣起来,目前围绕它有很多讨论,对于像笔者这样普通的外部企业 Java 开发者来说,并非所有讨论都易于理解。本文(本系列的第一篇)也许能帮助你理清这些讨论的重点。本文将尽量具体化,不涉及“指称语义学”。如果你想寻找更学术性的方法和大量的 Haskell 代码示例,网上有很多,但你可能来错地方了。

响应式编程经常被混淆为并发编程和高性能,以至于很难区分这些概念,而实际上它们在原则上完全不同。这不可避免地会导致混淆。响应式编程也经常被称为或混淆为函数式响应式编程,即 FRP(我们在本文中互换使用这两个术语)。有些人认为响应式并非新事物,而且他们每天都在使用(主要是使用 JavaScript)。另一些人似乎认为它是微软的恩赐(他们在一段时间前发布了一些 C# 扩展时对此大肆宣传)。在企业级 Java 领域,最近出现了一些热议(例如,参见响应式流规范),就像任何闪亮的新事物一样,关于何时何地可以使用和应该使用它,会犯很多简单的错误。

它是什么?

响应式编程是一种微架构风格,涉及事件的智能路由和消费,所有这些结合起来改变行为。这有点抽象,你在网上遇到的许多其他定义也是如此。接下来,我们将尝试建立一些更具体的概念,说明什么是响应式,或者为什么它可能很重要。

响应式编程的起源可能可以追溯到 20 世纪 70 年代甚至更早,所以这个概念本身并不新鲜,但它们确实在现代企业中产生了共鸣。这种共鸣并非偶然,它与微服务的兴起以及多核处理器的普及同时到来。希望这样做的原因能变得清晰一些。

以下是一些来自其他来源的精简定义

响应式编程背后的基本思想是存在某些数据类型,它们表示一个“随时间变化”的值。涉及这些随时间变化的值的计算本身也会产生随时间变化的值。

以及…​

一个很容易理解它的直观方法是,想象你的程序是一个电子表格,你的所有变量都是单元格。如果电子表格中的任何单元格发生变化,引用该单元格的所有单元格也会随之变化。FRP 也是如此。现在想象一下,有些单元格会自行变化(或者更确切地说,来自外部世界):在 GUI 环境中,鼠标的位置就是一个很好的例子。

(摘自Stackoverflow 上的术语问题)

FRP 与高性能、并发、异步操作和非阻塞 IO 有很强的关联性。然而,也许最好先怀疑一下 FRP 与这些概念毫无关系。在使用响应式模型时,这些问题确实可以自然地处理,并且通常对调用者是透明的。但是,在有效或高效处理这些问题方面的实际收益,完全取决于具体的实现(因此应该受到高度审视)。同样,也可以以同步、单线程的方式实现一个完全合理且有用的 FRP 框架,但这对于尝试使用任何新工具和库来说,可能并没有真正的帮助。

响应式应用场景

作为一个新手,似乎最难回答的问题是“它有什么用?” 这里有一些企业环境下的例子,说明了它的一般使用模式

外部服务调用 现今许多后端服务都是 REST-ful(即它们通过 HTTP 操作),因此底层协议本质上是阻塞和同步的。这可能不是 FRP 的明显领域,但实际上它是相当肥沃的土壤,因为这些服务的实现通常涉及调用其他服务,然后又依赖于第一次调用的结果来调用更多服务。如果 IO 如此频繁,而你在发送下一个请求之前等待一个调用完成,那么在你组织回复之前,你的可怜客户端就会沮丧地放弃。因此,外部服务调用,尤其是调用之间复杂的依赖编排,是一个很好的优化目标。FRP 提供了驱动这些操作的逻辑的“可组合性”承诺,使得调用服务的开发者更容易编写代码。

高并发消息消费者 消息处理,特别是在高并发的情况下,是企业常见的用例。响应式框架喜欢测量微基准测试,并吹嘘在 JVM 中每秒可以处理多少消息。结果确实惊人(每秒数千万条消息很容易实现),但也可能有点人工制造——如果他们说正在测试一个简单的“for”循环,你就不会那么惊讶了。然而,我们不应过早否定这些工作,很明显,当性能至关重要时,所有贡献都应欣然接受。响应式模式与消息处理天然契合(因为事件可以很好地转化为消息),因此如果有一种方法可以更快地处理更多消息,我们应该予以关注。

电子表格 也许它并非真正的企业用例,但却是企业中的每个人都能轻松理解的例子,它很好地抓住了 FRP 的理念和实现难度。如果单元格 B 依赖于单元格 A,而单元格 C 同时依赖于单元格 A 和 B,那么如何传播 A 中的变化,确保在向 B 发送任何变化事件之前更新 C 呢?如果你有一个真正的响应式框架作为基础,那么答案是“你不用管,你只需要声明依赖关系”,这就是电子表格力量的精髓所在。它也突出了 FRP 和简单事件驱动编程之间的区别——它赋予了“智能路由”以“智能”。

(非)同步处理的抽象 这更多是一个抽象的用例,因此有点偏离了我们也许应该避免的领域。它与前面提到的一些更具体的用例之间也存在一些(很多)重叠,但希望这仍然值得讨论。其基本论点是一个熟悉(且合理)的观点:只要开发者愿意接受额外的抽象层,他们就可以不必关心所调用的代码是同步还是异步的。由于处理异步编程会消耗宝贵的脑力,因此这里面可能有一些有用的想法。响应式编程不是解决这个问题唯一的方法,但 FRP 的一些实现者对这个问题进行了深入思考,他们的工具很有用。

这篇 Netflix 博客有一些非常有用的真实世界用例具体示例:Netflix 技术博客:使用 RxJava 在 Netflix API 中实现函数式响应式

比较

如果你不是自 1970 年以来一直住在山洞里,那么你肯定遇到过一些与其他响应式编程及其试图解决的问题相关的概念。以下是其中几个概念以及我个人对其相关性的看法

Ruby Event-Machine Event Machine 是对并发编程(通常涉及非阻塞 IO)的抽象。Ruby 开发者长期以来一直在努力将一种设计用于单线程脚本的语言转变为可以用来编写服务器应用程序的工具,这些应用程序需要 a) 工作正常,b) 性能良好,c) 在负载下保持稳定。Ruby 早已有了线程,但使用不多且名声不佳,因为它并非总能很好地工作。另一种选择是 Fibers(原文如此),它现在已经普及,并被提升(在 Ruby 1.9 中)到语言的核心。Fiber 编程模型有点像协程(参见下文)的一种变体,其中使用单个原生线程来处理大量并发请求(通常涉及 IO)。编程模型本身有点抽象且难以理解,因此大多数人使用包装器,而 Event Machine 是最常见的。Event Machine 不一定使用 Fibers(它抽象了这些问题),但在 Ruby Web 应用中很容易找到使用 Event Machine 与 Fibers 的代码示例(例如,参考 Ilya Grigorik 的这篇文章,或来自 em-http-request 的 Fibers 示例)。人们这样做主要是为了在 IO 密集型应用程序中使用 Event Machine 来获得可伸缩性的好处,同时避免大量嵌套回调带来的丑陋编程模型。

Actor 模型 与面向对象编程类似,Actor 模型是计算机科学中一个可追溯到 20 世纪 70 年代的深入探讨领域。Actor 提供了一种计算抽象(与数据和行为相对),这使得并发成为一种自然的推论,因此在实践中,它们可以构成并发系统的基础。Actor 之间相互发送消息,所以在某种意义上它们是响应式的,以 Actor 或响应式风格构建的系统之间有很多重叠。通常,区别在于它们的实现层面(例如,Akka 中的 Actor 可以跨进程分布,这是该框架的一个显著特性)。

延迟结果 (Futures) Java 1.5 引入了一套丰富的新库,包括 Doug Lea 的 "java.util.concurrent",其中一部分就是延迟结果的概念,封装在 Future 中。它是对异步模式进行简单抽象的一个很好的例子,它不强制实现必须是异步的,也不强制使用任何特定的异步处理模型。正如Netflix 技术博客:使用 RxJava 在 Netflix API 中实现函数式响应式很好地展示的,Futures 在只需要并发处理一组类似任务时非常有用,但一旦其中任何任务需要相互依赖或条件执行,就会陷入“嵌套回调地狱”。响应式编程提供了解决之道。

Map-reduce 和 Fork-Join 对并行处理的抽象很有用,并且有许多例子可供选择。Map-reduce 和 Fork-Join 是 Java 世界中最近演进的概念,它们受到了大规模并行分布式处理(MapReduceHadoop)以及 JDK 自身 1.7 版本(Fork-Join)的驱动。这些抽象很有用,但(就像延迟结果一样)与 FRP 相比它们是浅层的。FRP 可以用作简单并行处理的抽象,但它超越了这一点,达到了可组合性和声明式通信的层面。

协程 “协程” 是“子程序”的一种泛化——它像子程序一样有一个入口点和多个出口点,但当它退出时,它将控制权传递给另一个协程(不一定是它的调用者),并且它累积的任何状态都会保留并记住,以便下次调用时使用。协程可以作为构建更高级特性(如 Actor 和 Stream)的基础。响应式编程的目标之一是为并行处理的通信代理提供类似的抽象,因此协程(如果可用)是一个有用的构建块。协程有各种变体,其中一些比通用情况更具限制性,但比普通的子程序更灵活。Fibers(参见关于 Event Machine 的讨论)是一种变体,而 Generator(在 Scala 和 Python 中常见)是另一种。

Java 中的响应式编程

从原生不支持协程的角度来看,Java 不是一种“响应式语言”。JVM 上还有其他语言(Scala 和 Clojure)更原生地支持响应式模型,但 Java 本身直到版本 9 才支持。然而,Java 是企业开发的强大力量,最近有很多活动致力于在 JDK 之上提供响应式层。我们在这里只对其中几个进行非常简要的介绍。

响应式流规范 是一个非常低层次的契约,以少量 Java 接口(加上一个 TCK)的形式表达,但也适用于其他语言。这些接口表达了具有显式背压(back pressure)的 PublisherSubscriber 的基本构建块,为可互操作的库形成了一种通用语言。响应式流规范已在版本 9 中作为 java.util.concurrent.Flow 集成到 JDK 中。该项目是 Kaazing、Netflix、Pivotal、Red Hat、Twitter、Typesafe 以及许多其他公司的工程师共同协作的成果。

RxJava:Netflix 一段时间以来内部一直在使用响应式模式,然后他们将使用的工具以开源许可证发布为 Netflix/RxJava(后来更名为“ReactiveX/RxJava”)。Netflix 大量使用 Groovy 在 RxJava 之上进行编程,但它也支持 Java 使用,并且通过使用 Lambda 表达式非常适合 Java 8。存在一个连接到响应式流规范的桥接。根据 David Karnok 的响应式库代系分类,RxJava 是一个“第二代”库。

Reactor 是由 Pivotal 开源团队(即创建 Spring 的团队)开发的一个 Java 框架。它直接基于响应式流规范构建,因此无需桥接。Reactor IO 项目提供了对 Netty 和 Aeron 等低级网络运行时环境的封装。根据 David Karnok 的响应式库代系分类,Reactor 是一个“第四代”库。

Spring Framework 5.0(第一个里程碑版本于 2016 年 6 月发布)内置了响应式特性,包括构建 HTTP 服务器和客户端的工具。现有的 Web 层 Spring 用户会发现一个非常熟悉的编程模型,使用注解修饰控制器方法来处理 HTTP 请求,大部分响应式请求的分发和背压问题都交给框架处理。Spring 基于 Reactor 构建,但也暴露了允许使用多种库(例如 Reactor 或 RxJava)来表达其特性的 API。用户可以从 Tomcat、Jetty、Netty(通过 Reactor IO)和 Undertow 中选择服务器端网络栈。

Ratpack 是一套用于构建高性能 HTTP 服务的库。它基于 Netty 构建,并实现了响应式流规范以实现互操作性(因此你可以在栈中更上层使用其他的响应式流规范实现,例如)。Spring 作为原生组件得到支持,可以使用一些简单的工具类提供依赖注入。还存在一些自动配置,使得 Spring Boot 用户可以将 Ratpack 嵌入到 Spring 应用程序中,从而启动一个 HTTP 端点并在那里监听,而不是使用 Spring Boot 直接提供的嵌入式服务器之一。

Akka 是一个用于在 Scala 或 Java 中使用 Actor 模式构建应用程序的工具包,使用 Akka Streams 进行进程间通信,并且内置了响应式流规范契约。根据 David Karnok 的响应式库代系分类,Akka 是一个“第三代”库。

为何是现在?

是什么推动了企业级 Java 中响应式的兴起?嗯,这不仅仅是(全部)技术时尚——人们赶时髦,追逐闪亮的新玩具。驱动力是高效的资源利用,换句话说,就是在服务器和数据中心上花费更少的钱。响应式承诺你可以用更少的资源做更多的事情,具体来说,你可以用更少的线程处理更高的负载。这就是响应式与非阻塞、异步 I/O 的交集凸显出来的地方。对于正确的问题,效果是显著的。对于错误的问题,效果可能会适得其反(你实际上让事情变得更糟)。此外,请记住,即使你选择了正确的问题,也没有免费的午餐,响应式并不能帮你解决问题,它只是给你提供了一个工具箱,你可以用它来实现解决方案。

结论

在本文中,我们对响应式运动进行了非常广泛和高层次的探讨,并将其置于现代企业的背景下。JVM 上有许多响应式库或框架,它们都处于积极开发中。很大程度上,它们提供相似的功能,但越来越多地,得益于响应式流规范,它们能够互操作。在本系列下一篇文章中,我们将深入实际,查看一些真实的代码示例,以便更好地了解响应式的具体含义及其重要性。我们还将花一些时间来理解 FRP 中“F”为何重要,以及背压和非阻塞代码的概念如何深刻影响编程风格。最重要的是,我们将帮助你做出重要决定:何时以及如何转向响应式,以及何时坚持使用旧的风格和技术栈。

获取 Spring 新闻邮件

订阅 Spring 新闻邮件,保持联系

订阅

领先一步

VMware 提供培训和认证,助你快速提升。

了解更多

获取支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一次简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部