响应式编程笔记 第一部分:响应式生态系统

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

响应式编程(Reactive Programming)又一次引起了人们的兴趣,目前关于它的讨论非常多,但对于像作者这样的外部人士和普通的企业 Java 开发人员来说,这些讨论并不那么容易理解。本文(系列文章的第一篇)可能有助于澄清你对这场喧嚣的理解。本文的方法尽可能具体,并且不会提及“指示性语义”。如果你正在寻找更学术的方法和大量的 Haskell 代码示例,互联网上有很多,但你可能不应该在这里。

响应式编程常常与并发编程和高性能混淆,以至于很难将这些概念区分开来,而实际上它们原则上是完全不同的。这不可避免地导致了混淆。响应式编程也经常被称为或混淆为函数式响应式编程(Functional Reactive Programming,简称 FRP),(在本文中我们互换使用这两个术语)。有些人认为响应式编程没什么新鲜的,他们每天都在做(主要是使用 JavaScript)。另一些人似乎认为它是微软的一项馈赠(微软在不久前发布了一些 C# 扩展时,曾为此大肆宣传)。在企业 Java 领域,最近出现了一些动静(例如,请参阅 Reactive Streams initiative),正如任何闪亮的新事物一样,在何时何地可以使用和应该使用它方面,有很多容易犯的错误。

这是什么?

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

响应式编程的起源可能可以追溯到 20 世纪 70 年代甚至更早,所以这个想法并不新鲜,但它却与现代企业中的一些事物产生了共鸣。这种共鸣(并非偶然)与微服务的兴起和多核处理器的普及同时发生。希望其中一些原因将在后面变得清晰。

以下是一些来自其他来源的有用的小定义

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

还有…

要获得对此的初步直观认识,一个简单的方法是想象你的程序是一个电子表格,而你的所有变量都是单元格。如果电子表格中的任何一个单元格发生变化,任何引用该单元格的单元格也会随之变化。这与 FRP 是一样的。现在想象一下,有些单元格是自己变化的(或者更确切地说,是从外部世界获取的):在 GUI 环境中,鼠标的位置就是一个很好的例子。

(来自 Stackoverflow 上的术语问题

FRP 与高性能、并发、异步操作和非阻塞 I/O 具有很强的亲和性。然而,从怀疑 FRP 与这些完全无关的角度开始可能是有益的。当使用响应式模型时,这些问题当然可以被自然地处理,通常对调用者来说是透明的。但是,在有效或高效处理这些问题方面的实际好处完全取决于具体的实现(因此应该受到高度审查)。也可以实现一个完全合理且有用的 FRP 框架,采用同步、单线程的方式,但这在尝试使用任何新工具和库时可能并没有什么帮助。

响应式用例

对于新手来说,最难回答的问题似乎是“它有什么用?”以下是一些企业场景的示例,说明了其通用使用模式

外部服务调用 当今许多后端服务都是 RESTful 的(即,它们通过 HTTP 运行),因此底层协议本质上是阻塞的和同步的。对于 FRP 来说,这可能不是一个显而易见的应用领域,但实际上它是一个相当肥沃的领域,因为实现此类服务通常涉及调用其他服务,然后根据第一个调用的结果又调用更多服务。由于有大量的 I/O 操作,如果你要等待一个调用完成,然后才发送下一个请求,那么在你能组合出一个回复之前,可怜的客户端就会因为沮丧而放弃。因此,外部服务调用,特别是复杂的依赖关系编排,是需要优化的好地方。FRP 提供了驱动这些操作的逻辑的“可组合性”的承诺,从而使调用服务的开发人员更容易编写代码。

高并发消息消费者 消息处理,特别是高并发的消息处理,是企业中常见的用例。响应式框架喜欢衡量微基准测试,并吹嘘在 JVM 中每秒可以处理多少条消息。结果确实惊人(每秒数千万条消息很容易实现),但可能有些人为——如果它们说在对一个简单的“for”循环进行基准测试,你可能就不会那么印象深刻了。然而,我们不应该过早地否定这类工作,并且很容易看到,当性能很重要时,所有的贡献都应该被感激地接受。响应式模式很自然地适合消息处理(因为事件可以很好地转换为消息),所以如果有一种方法可以更快地处理更多消息,我们应该引起注意。

电子表格 这可能不是一个真正的企业用例,但每个企业中的人都能轻易地理解它,而且它很好地捕捉了 FRP 的理念和实现难度。如果单元格 B 依赖于单元格 A,而单元格 C 依赖于单元格 A 和 B,那么如何传播 A 的变化,确保在将任何更改事件发送到 B 之前更新 C?如果你有一个真正响应式的框架来构建,那么答案是“你不用关心,你只需要声明依赖关系”,这实际上就是电子表格的强大之处。它也突显了 FRP 和简单的事件驱动编程之间的区别——它在“智能路由”中赋予了“智能”的含义。

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

这个 Netflix 博客提供了一些非常实用的实际用例的示例:Netflix Tech Blog: Functional Reactive in the Netflix API with RxJava

比较

如果你从 1970 年起就没有隐居起来,那么你一定会遇到一些与响应式编程及其试图解决的问题相关的概念。这里有几个,以及我对它们相关性的个人看法

Ruby Event-Machine Event Machine 是对并发编程(通常涉及非阻塞 I/O)的一种抽象。Rubyist 花了很长时间才将一种专为单线程脚本设计的语言转变为可以用来编写a)工作正常、b)性能良好、c)在负载下保持运行的服务器应用程序。Ruby 已经有了线程一段时间,但它们并不常被使用,而且声誉不佳,因为它们并不总是表现良好。替代方案是现在已经推广到语言核心(在 Ruby 1.9 中)的Fibers( sic)。Fiber 编程模型是一种协程(coroutines)的变体(见下文),其中单个原生线程用于处理大量并发请求(通常涉及 I/O)。编程模型本身有点抽象,难以推理,所以大多数人使用包装器,而 Event Machine 是最常见的。Event Machine 不一定使用 Fibers(它抽象了这些关注点),但在 Ruby Web 应用中很容易找到使用 Event Machine 和 Fibers 的代码示例(例如,参见 Ilya Grigorik 的这篇文章,或者em-http-request 中的 fibered 示例)。人们这样做是为了在 I/O 密集型应用程序中使用 Event Machine 获得可扩展性的好处,而无需使用大量嵌套回调的丑陋编程模型。

Actor 模型 类似于面向对象编程,Actor 模型是计算机科学的一个深度分支,可以追溯到 20 世纪 70 年代。Actor 提供了一种计算(而不是数据和行为)的抽象,允许并发作为其自然结果,因此在实践中它们可以构成并发系统的基础。Actor 之间发送消息,所以它们在某种意义上是响应式的,并且自称为 Actor 或 Reactive 的系统之间有很多重叠。通常区别在于它们的实现级别(例如,Akka 中的 `Actors` 可以分布在不同的进程中,这是该框架的一个显著特征)。

延迟结果(Futures) Java 1.5 引入了一套丰富的新库,包括 Doug Lea 的“java.util.concurrent”,其中包含了一个延迟结果的概念,封装在 `Future` 中。它是一个在异步模式之上进行简单抽象的良好示例,而不强制实现必须是异步的,或使用任何特定的异步处理模型。正如 Netflix Tech Blog: Functional Reactive in the Netflix API with RxJava 中所展示的那样,当您只需要并发处理一组类似的任务时,`Futures` 非常有用,但一旦其中任何一个任务需要相互依赖或有条件地执行,您就会陷入一种“嵌套回调地狱”。响应式编程为这种困境提供了解药。

Map-reduce 和 fork-join 并行处理的抽象很有用,并且有很多例子可供选择。Map-reduce 和 fork-join 最近在 Java 世界中得到发展,这得益于大规模并行分布式处理(MapReduceHadoop)以及 JDK 本身在 1.7 版本中(Fork-Join)。这些是实用的抽象,但(与延迟结果一样)它们与 FRP 相比很浅显,FRP 可以用作简单并行处理的抽象,但它超越了这一点,达到了可组合性和声明式通信。

协程 “协程”(coroutine)是“子例程”(subroutine)的泛化——它有一个入口点和一个或多个出口点,就像子例程一样,但当它退出时,它会将控制权传递给另一个协程(不一定是其调用者),并且它积累的任何状态都会被保存并记住,以便下次调用时使用。协程可以用作 Actor 和 Streams 等更高级功能的构建块。响应式编程的目标之一是为通信的并行处理代理提供相同类型的抽象,因此协程(如果可用)是有用的构建块。协程有各种变体,其中一些比一般情况更受限制,但比普通的子例程更灵活。Fibers(参见关于 Event Machine 的讨论)是一种变体,而 Generators(Scala 和 Python 中很熟悉)是另一种。

Java 中的响应式编程

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

Reactive Streams 是一个非常底层的契约,表示为 handful 的 Java 接口(加上一个 TCK),但也可应用于其他语言。这些接口通过显式的反压(back pressure)表达了 `Publisher` 和 `Subscriber` 的基本构建块,为可互操作的库形成了一种通用语言。Reactive Streams 已在 9.0 版本中作为 `java.util.concurrent.Flow` 被纳入 JDK。该项目是 Kaazing、Netflix、Pivotal、Red Hat、Twitter、Typesafe 等众多工程师的合作成果。

RxJava:Netflix 在内部使用响应式模式已有一段时间,然后他们将使用的工具以开源许可证的形式发布,即 Netflix/RxJava(随后更名为“ReactiveX/RxJava”)。Netflix 使用 RxJava 进行大量的 Groovy 编程,但它也支持 Java,并且通过 Lambda 非常适合 Java 8。有一个连接到 Reactive Streams 的桥接器。根据 David Karnok 的《Reactive 的代际》分类,RxJava 是一个“第二代”库。

Reactor 是来自 Pivotal 开源团队(创建 Spring 的团队)的 Java 框架。它直接构建在 Reactive Streams 之上,因此无需桥接器。Reactor IO 项目提供了对 Netty 和 Aeron 等底层网络运行时的包装。根据 David Karnok 的《Reactive 的代际》分类,Reactor 是一个“第四代”库。

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

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

Akka 是一个使用 Scala 或 Java 中的 Actor 模式构建应用程序的工具包,通过 Akka Streams 进行进程间通信,并内置了 Reactive Streams 合约。根据 David Karnok 的《Reactive 的代际》分类,Akka 是一个“第三代”库。

为什么是现在?

是什么推动了响应式在企业 Java 中的兴起?嗯,这(不完全)只是一种技术潮流——人们只是搭上了新潮玩具的顺风车。驱动力是有效的资源利用,换句话说,就是减少在服务器和数据中心上的花费。响应式的承诺是,你可以用更少的资源做更多的事情,特别是你可以用更少的线程处理更高的负载。这就是响应式与非阻塞、异步 I/O 相交的地方。对于正确的问题,效果是惊人的。对于错误的问题,效果可能会适得其反(你实际上会让事情变得更糟)。还要记住,即使你选择了正确的问题,天下也没有免费的午餐,响应式并不能为你解决问题,它只是提供了一个你可以用来实现解决方案的工具箱。

结论

在本文中,我们对响应式运动进行了非常广泛和高层次的概述,将其置于现代企业的背景下。JVM 上有许多响应式库或框架,都处于积极开发中。在很大程度上,它们提供了相似的功能,但由于 Reactive Streams 的存在,它们越来越互操作。在下一篇文章中,我们将深入探讨实际代码示例,以更清楚地了解响应式意味着什么以及为什么它很重要。我们还将花一些时间来理解为什么 FRP 中的“F”很重要,以及反压和非阻塞代码的概念如何深刻影响编程风格。最重要的是,我们将帮助您做出重要决定,何时以及如何采用响应式,以及何时保持使用旧的样式和堆栈。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

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

了解更多

即将举行的活动

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

查看所有