领先一步
VMware 提供培训和认证来加速您的进步。
了解更多响应式编程很有趣(再次),目前有很多关于它的讨论,对于像作者这样的外行和简单的企业 Java 开发人员来说,并非所有讨论都容易理解。本文(系列文章的第一篇)可能有助于澄清您对这种热潮的理解。方法尽可能具体,并且没有提到“指称语义”。如果您正在寻找更学术的方法和大量的 Haskell 代码示例,互联网上有很多,但您可能不想在这里找到它们。
响应式编程通常与并发编程和高性能混淆,以至于很难将这些概念区分开来,而实际上它们在原则上是完全不同的。这不可避免地导致混淆。响应式编程也常被称为或与函数式响应式编程或 FRP 混淆(在这里我们交替使用这两个术语)。有些人认为响应式编程没有什么新意,这正是他们每天都在做的事情(他们大多使用 JavaScript)。其他人似乎认为它是微软的礼物(当他们几年前发布了一些 C# 扩展时,他们对此大肆宣传)。在企业 Java 领域,最近出现了一些热议(例如,参见Reactive Streams 计划),并且像任何闪亮的新事物一样,在何时何地可以以及应该使用它方面,存在很多容易犯的错误。
响应式编程是一种微架构风格,涉及事件的智能路由和消费,所有这些都结合起来改变行为。这有点抽象,而且您在网上遇到的许多其他定义也是如此。我们尝试建立一些关于响应式含义或为什么它在后续内容中可能很重要的更具体的概念。
响应式编程的起源可能可以追溯到 20 世纪 70 年代甚至更早,所以这个想法并没有什么新意,但它们确实与现代企业中的某些东西产生了共鸣。这种共鸣(并非偶然地)出现在微服务的兴起和多核处理器的普遍使用之时。希望其中的一些原因会变得清晰。
以下是一些来自其他来源的有用简要定义
响应式编程的基本思想是,某些数据类型表示“随时间推移”的值。涉及这些随时间变化的值的计算本身也将具有随时间变化的值。
以及……
一种获得对它是什么感觉的第一直觉的简单方法是,想象一下您的程序是一个电子表格,而所有变量都是单元格。如果电子表格中的任何单元格发生变化,任何引用该单元格的单元格也会发生变化。FRP 也是如此。现在想象一下,有些单元格会自行更改(或者更确切地说,是从外部世界获取的):在 GUI 情况下,鼠标的位置就是一个很好的例子。
FRP 与高性能、并发、异步操作和非阻塞 IO 密切相关。但是,从怀疑 FRP 与它们没有任何关系开始可能会有所帮助。当然,在使用响应式模型时,通常可以透明地处理这些问题。但是,在有效或高效地处理这些问题的方面,实际的好处完全取决于所讨论的实现(因此应该受到高度审查)。也可以以同步、单线程的方式实现一个完全合理且有用的 FRP 框架,但这对于尝试使用任何新的工具和库来说不太可能有所帮助。
对于新手来说,最难回答的问题似乎是“它有什么用?”以下是一些来自企业环境的示例,说明了常见的用例模式
外部服务调用如今,许多后端服务都是 RESTful 的(即它们通过 HTTP 运行),因此底层协议从根本上说是阻塞的和同步的。也许对于 FRP 来说不是显而易见的领域,但实际上它是一个非常肥沃的土壤,因为此类服务的实现通常涉及调用其他服务,然后根据第一次调用的结果调用更多服务。如果在发送下一个请求之前等待一个调用完成,那么您的可怜的客户端在您设法组装回复之前就会因为沮丧而放弃,因此存在大量的 IO 操作。因此,外部服务调用,特别是调用之间依赖关系的复杂编排,是一件需要优化的事情。FRP 承诺能够“组合”驱动这些操作的逻辑,从而使调用服务的开发人员更容易编写。
高并发消息消费者消息处理,尤其是在高并发的情况下,是常见的企业用例。响应式框架喜欢衡量微基准测试,并吹嘘您可以在 JVM 中处理多少消息每秒。结果确实惊人(每秒处理数千万条消息很容易实现),但可能有点人为——如果他们说他们正在对一个简单的“for”循环进行基准测试,您就不会那么印象深刻。但是,我们不应该太快地否定这样的工作,并且很容易看出,当性能很重要时,应该欣然接受所有贡献。响应式模式自然适合消息处理(因为事件很好地转换为消息),因此,如果有一种方法可以更快地处理更多消息,我们应该注意。
电子表格也许这不是一个真正的企业用例,但企业中的每个人都可以轻松理解它,它很好地捕捉了 FRP 的理念和实施的难度。如果单元格 B 依赖于单元格 A,而单元格 C 依赖于单元格 A 和 B,那么您如何传播 A 中的变化,确保在向 B 发送任何更改事件之前更新 C?如果您有一个真正响应式的框架来构建,那么答案是“您不用担心,您只需声明依赖关系”,这正是电子表格的核心力量。它还突出了 FRP 和简单事件驱动编程之间的区别——它在“智能路由”中加入了“智能”。
(A)同步处理的抽象这更像是一个抽象的用例,因此偏离了我们可能应该避免的领域。它与前面提到的更具体的用例之间也有一些(很多)重叠,但希望它仍然值得讨论。基本主张是一个熟悉(且合理的)主张,即只要开发人员愿意接受额外的抽象层,他们就可以忘记他们正在调用的代码是同步的还是异步的。由于处理异步编程需要宝贵的脑细胞,因此可能存在一些有用的想法。响应式编程不是解决此问题的唯一方法,但一些 FRP 的实施者对这个问题思考得足够深入,因此他们的工具很有用。
此 Netflix 博客提供了一些关于现实生活中用例的非常有用的具体示例:Netflix 技术博客:使用 RxJava 在 Netflix API 中进行函数式响应式编程
如果您自 1970 年以来一直没有生活在洞穴里,您会遇到一些与响应式编程相关的其他概念以及人们试图用它解决的问题类型。以下是一些我个人认为与之相关的概念
Ruby Event-Machine Event Machine 是并发编程(通常涉及非阻塞IO)的一种抽象。Ruby 开发者长期以来一直在努力将一种为单线程脚本设计的语言转变为可以用来编写服务器应用程序的东西,该应用程序 a) 工作正常,b) 性能良好,以及 c) 在负载下保持存活。Ruby 已经拥有线程相当长一段时间了,但它们的使用并不多,并且声誉不佳,因为它们的性能并不总是很好。另一种选择,现在已经成为语言核心(在 Ruby 1.9 中),并且无处不在,是Fiber(原文如此)。Fiber 编程模型有点像协程(见下文),其中单个原生线程用于处理大量并发请求(通常涉及 IO)。编程模型本身有点抽象,难以理解,因此大多数人使用包装器,而 Event Machine 是最常见的。Event Machine 不一定使用 Fiber(它抽象了这些问题),但很容易找到在 Ruby Web 应用中使用 Fiber 与 Event Machine 的代码示例(例如,参见 Ilya Grigorik 的这篇文章,或em-http-request 中的 Fiber 示例)。人们经常这样做是为了获得在 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 技术博客:使用 RxJava 在 Netflix API 中进行函数式反应式编程所示,当您只需要并发处理一组类似的任务时,Futures
非常棒,但是一旦其中任何一个想要相互依赖或有条件地执行,您就会陷入一种“嵌套回调地狱”的形式。反应式编程为此提供了解药。
Map-Reduce 和 Fork-Join 对并行处理的抽象很有用,并且有很多例子可供选择。Map-Reduce 和 Fork-Join 最近在 Java 世界中发展起来,由大规模并行分布式处理(MapReduce 和 Hadoop)以及 JDK 本身在 1.7 版本中(Fork-Join)推动。这些是有用的抽象,但(与延迟结果一样)它们与 FRP 相比是浅层的,FRP 可以用作简单并行处理的抽象,但它超越了这一点,进入了可组合性和声明式通信。
协程 “协程” 是“子程序”的泛化——它像子程序一样具有入口点和出口点,但在退出时,它将控制权传递给另一个协程(不一定传递给其调用方),并且它累积的任何状态都会被保留并记住,以便下次调用时使用。协程可以用作更高级功能(如 Actor 和流)的构建块。反应式编程的目标之一是提供相同类型的抽象,用于通信并行处理代理,因此协程(如果可用)是一个有用的构建块。协程有各种各样的风格,其中一些比一般情况更具限制性,但比普通的子程序更灵活。Fiber(参见关于 Event Machine 的讨论)是一种风格,而 Generator(在 Scala 和 Python 中很熟悉)是另一种。
Java 并不是一种“反应式语言”,因为它本身不支持协程。JVM 上还有其他语言(Scala 和 Clojure)更原生地支持反应式模型,但 Java 本身在 9 版本之前都不支持。然而,Java 是企业开发的强大力量,最近在 JDK 之上提供反应式层方面有很多活动。我们在这里只简要介绍其中几个。
Reactive Streams 是一种非常底层的契约,表示为少量 Java 接口(加上 TCK),但也适用于其他语言。这些接口表达了Publisher
和 Subscriber
的基本构建块,并具有明确的反压,形成了用于互操作库的通用语言。Reactive Streams 已在 9 版本中作为 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 的反应式编程的世代分类,RxJava 是一个“第二代”库。
Reactor 是来自 Pivotal 开源团队(创建 Spring 的团队)的 Java 框架。它直接构建在 Reactive Streams 之上,因此不需要桥梁。Reactor IO 项目围绕 Netty 和 Aeron 等底层网络运行时提供了包装器。根据 David Karnok 的反应式编程的世代分类,Reactor 是一个“第四代”库。
Spring Framework 5.0(第一个里程碑是 2016 年 6 月)在其内置了反应式特性,包括用于构建 HTTP 服务器和客户端的工具。Web 层中现有的 Spring 用户会发现一个非常熟悉的编程模型,使用注解来装饰控制器方法以处理 HTTP 请求,在大多数情况下,将反应式请求的分发和反压问题交给框架处理。Spring 构建在 Reactor 之上,但也公开了 API,允许使用选择的库(例如 Reactor 或 RxJava)来表达其功能。用户可以从 Tomcat、Jetty、Netty(通过 Reactor IO)和 Undertow 中选择服务器端网络栈。
Ratpack 是一套用于构建基于 HTTP 的高性能服务的库。它构建在 Netty 之上,并为互操作性实现了 Reactive Streams(因此您可以在堆栈的上层使用其他 Reactive Streams 实现,例如)。Spring 作为原生组件受支持,并且可以用于使用一些简单的实用程序类提供依赖注入。还有一些自动配置,以便 Spring Boot 用户可以在 Spring 应用程序中嵌入 Ratpack,启动一个 HTTP 端点并在那里监听,而不是使用 Spring Boot 直接提供的嵌入式服务器之一。
Akka 是一款使用 Scala 或 Java 中的 Actor 模式构建应用程序的工具包,使用 Akka Streams 进行进程间通信,并内置了 Reactive Streams 契约。根据 David Karnok 的反应式编程的世代分类,Akka 是一个“第三代”库。
是什么推动了反应式在企业 Java 中的兴起?好吧,它(并非全部)仅仅是一个技术潮流——人们带着闪亮的新玩具加入潮流。驱动力是高效的资源利用,或者换句话说,减少在服务器和数据中心上的支出。反应式的承诺是您可以用更少的资源做更多的事情,具体来说,您可以用更少的线程处理更高的负载。这就是反应式与非阻塞异步 I/O 交叉的地方。对于正确的问题,效果是显著的。对于错误的问题,效果可能会反过来(你实际上会让事情变得更糟)。还要记住,即使您选择了正确的问题,也没有免费的午餐,反应式并不能为您解决问题,它只是为您提供了一个可以用来实现解决方案的工具箱。
在这篇文章中,我们对反应式运动进行了非常广泛和高级别的概述,将其置于现代企业的背景下。JVM 有许多反应式库或框架,所有这些都在积极开发中。在很大程度上,它们提供了类似的功能,但越来越多的,由于 Reactive Streams,它们是互操作的。在本系列的下一篇文章中,我们将深入探讨并查看一些实际的代码示例,以更好地了解成为反应式以及为什么重要的具体含义。我们还将花费一些时间来理解 FRP 中的“F”为什么很重要,以及反压和非阻塞代码的概念如何对编程风格产生深远的影响。最重要的是,我们将帮助您做出关于何时以及如何转向反应式以及何时坚持使用旧的风格和堆栈的重要决定。