vlambda博客
学习文章列表

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

第 1 章 为什么选择反应弹簧?

在本章中,我们将解释 reactivity 的概念,看看为什么反应式方法优于传统方法。为此,我们将查看传统方法失败的示例。除此之外,我们还将探讨构建稳健系统的基本原则,该系统通常被称为 反应式系统。我们还将概述在分布式服务器之间构建消息驱动通信的概念原因,涵盖适合反应性的业务案例。然后,我们将 扩展响应式编程的含义,构建一个细粒度的响应式系统。  我们还将讨论 为什么 Spring 框架团队 决定将响应式方法作为核心部分 Spring 框架 5。根据本章的内容,我们将了解反应性的重要性以及为什么将我们的项目转移到反应性世界是一个好主意。

在本章中,我们将介绍以下主题:

  • 为什么我们需要反应性

  • The fundamental principles of the reactive system
  • Business cases in which a reactive system design matches perfectly
  • Programming techniques that are more suitable for a reactive system
  • Reasons for moving Spring Framework to reactivity

为什么是反应式的?


如今,reactive 是一个流行词——非常令人兴奋但是< /a> 太混乱了。然而,我们是否仍然应该关心 反应性 即使它在世界各地的会议中占有一席之地?如果我们用谷歌搜索反应这个词,我们会发现最流行的联想是编程

,

 其中定义了编程模型的含义。然而,这并不是反应性的唯一含义。在这个词的背后,隐藏着旨在 构建强大系统的基本设计原则。 为了理解反应性作为基本设计原则的价值,让我们假设我们正在开发一家小型企业。

假设我们的小型企业是一家网上商店,拥有一些价格诱人的尖端产品。与该领域的大多数项目一样,我们将聘请软件工程师来解决我们遇到的任何问题。我们选择了传统的开发方法,并且在几次开发互动中,我们创建了我们的商店。

通常,每小时约有一千名用户访问我们的服务。为了满足通常的需求,我们购买了一台现代计算机并运行了 Tomcat Web 服务器,并为 Tomcat 的线程池配置了 500 个分配的线程。大多数用户请求的平均响应时间约为 250 毫秒。通过简单地计算该配置的容量,我们可以确定系统每秒可以处理大约 2,000 个用户请求。据统计,前面提到的用户数量平均每秒产生约 1000 个请求。因此,当前系统的容量将足以满足平均负载。

总而言之,我们在容量方面配置了我们的应用程序。此外,我们的网上商店一直稳定运行到 11 月的最后一个星期五,也就是黑色星期五。

黑色星期五对客户和零售商来说都是宝贵的一天。对于客户来说,这是一个以折扣价购买商品的机会。而对于零售商来说,这是一种赚钱和推广产品的方式。然而,这一天的特点是客户异常涌入,这可能是生产失败的重要原因。

当然,我们失败了!在某个时间点,负载超出了所有预期。线程池中没有空闲线程来处理用户请求。反过来, 备份服务器无法处理这种不可预测的入侵,最终导致响应时间增加和定期服务中断。在这一点上,我们开始失去一些用户请求,最后,我们的客户变得不满意并更愿意与竞争对手打交道。

最终,很多潜在客户和金钱都流失了,商店的评分也下降了。这都是因为我们无法在工作量增加的情况下保持响应。

但是,别担心,这并不是什么新鲜事。曾几何时,亚马逊和沃尔玛等巨头也面临过这个问题,并且已经找到了解决方案。尽管如此,我们仍将遵循与前人相同的道路,了解设计稳健系统的核心原则,然后为它们提供一般定义。

笔记

要了解有关巨人失败的更多信息,请参阅:

现在,应该 留在我们脑海中的核心问题是——我们应该如何 响应? 正如我们现在所做的那样从前面给出的示例中可以理解,应用程序应该对更改做出反应。这应该包括需求(负载)的变化和外部服务可用性的变化。换句话说,它应该对任何可能影响系统响应用户请求的能力的变化做出反应。

实现主要目标的首要方法之一是通过弹性。这描述了能力在变化的工作负载下保持响应,这意味着系统的吞吐量应该在更多用户时自动增加 开始使用它,当需求下降时,它应该会自动减少。从应用程序的角度来看,此功能可实现系统响应能力,因为在任何时间点都可以扩展系统而不会影响平均延迟。

笔记

请注意,latency 是响应性的基本特征。没有弹性,增长的需求会导致平均延迟的增长,直接影响系统的响应能力。

例如,通过提供额外的计算资源或额外的实例,我们系统的吞吐量可能会增加。响应性将因此增加。另一方面,如果需求低,系统应该在资源消耗方面收缩,从而减少业务费用。我们可以通过使用可扩展性来实现弹性,这可能 be horizo​​ntal 或垂直。然而,实现 分布式系统的 可扩展性是一项挑战,通常受限于系统内引入的瓶颈或同步点。  ;从理论和实践的角度来看,这些问题用阿姆达尔定律和冈瑟的通用可扩展性模型来解释。我们将在第 6 章中讨论这些内容,WebFlux 异步非阻塞通信

 

笔记

在这里,术语业务费用指的是额外的云实例的成本或物理机器的额外功耗。

然而,构建一个可扩展的分布式系统却无法在任何故障下都保持响应的能力 是一个挑战。让我们考虑一下我们系统的某个部分不可用的情况。在这里,外部支付服务出现故障,所有用户支付商品的尝试都将失败。这会破坏系统的响应能力,在某些情况下可能是不可接受的。例如,如果用户无法轻松进行购买,他们可能会去竞争对手的网上商店。为了提供高质量的用户体验,我们必须关心系统的响应能力。系统的验收标准是在故障下保持响应的能力,或者换句话说,具有弹性。这可以通过在系统的功能组件之间应用隔离来实现,从而隔离所有内部故障并实现独立性。让我们切换回亚马逊网上商店。亚马逊有很多不同的功能组件,例如订单列表、支付服务、广告服务、评论服务等等。例如,在支付服务中断的情况下,我们可能会接受用户订单,然后安排请求自动重试,从而保护用户免受意外失败的影响。另一个示例可能是与评论服务隔离。如果评论服务出现故障,采购和订单列表服务应该不会受到影响,并且应该可以正常工作。

需要强调的另一点是弹性和弹性是紧密耦合的,只有同时启用两者才能实现真正的响应式系统。 通过可扩展性,我们可以拥有 multiple 组件的副本,这样,如果一个失败,我们可以检测 这个,最小化它对系统restswitch 到另一个副本。

 

消息驱动的通信

剩下的唯一问题unclear 是如何连接分布式系统中的组件并保持解耦、隔离和可扩展性。同时。让我们考虑一下组件之间通过 HTTP 进行的通信。下一个代码示例,在 Spring Framework 4 中进行 HTTP 通信,代表了这个概念:

@RequestMapping("/resource")                                       // (1)
public Object processRequest() {
    RestTemplate template = new RestTemplate();                    // (2)

    ExamplesCollection result = template.getForObject(             // (3) 
       "http://example.com/api/resource2",                         // 
       ExamplesCollection.class                                    //
    );                                                             //

    ...                                                            // (4)

    processResultFurther(result);                                  // (5)
}

前面的代码解释如下:

  1. 此时的代码是使用  @RequestMapping 注解的请求处理程序映射声明。
  2. 此块中声明的代码显示了我们如何创建 RestTemplate 实例。 RestTemplate 是 Spring Framework 4 中用于在服务之间进行请求-响应通信的最流行的 Web 客户端。
  3. 这演示了请求的构造和执行。在这里,使用 RestTemplate API,我们构造了一个 HTTP 请求并在此之后立即执行它。请注意,响应将自动映射到 Java 对象并作为执行结果返回。响应体的类型由 getForObject 方法的第二个参数定义。此外,getXxxXxxxxx 前缀意味着在这种情况下 HTTP 方法是 GET
  4. 这些是前面示例中跳过的附加操作。
  5. 这是另一个处理阶段的执行。

 

在前面的示例中,我们定义了将在用户请求时调用的请求处理程序。反过来,处理程序的每次调用都会对外部服务产生额外的 HTTP 调用,然后执行另一个处理阶段。尽管前面的代码在逻辑上看起来很熟悉且透明,但它还是有一些缺陷。要了解此示例中的问题所在,让我们大致了解一下以下 请求的时间线:

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

图 1.1。组件交互时间线

此图描述了相应代码的实际行为。正如我们可能注意到的,只有一小部分处理时间被分配用于有效的 CPU 使用,而其余时间线程被 I/O 阻塞,不能用于处理其他请求。

笔记

在某些语言中,例如 C#、Go 和 Kotlin,当使用绿色线程时,相同的代码可能是非阻塞的。但是,在纯 Java 中,我们还没有这样的特性。因此,在这种情况下,实际线程将被阻塞。

另一方面,在 Java 世界中,我们有 thread 池,它们可以分配额外的线程来增加并行处理。然而,在高负载下,这种技术可能无法同时处理新的 I/O 任务。我们将在本章中再次讨论这个问题,并在 第 6 章WebFlux 异步非阻塞通信。

尽管如此,我们可以同意,为了在 I/O 情况下实现更好的资源利用率,我们应该使用异步和非阻塞交互模型。在现实生活中,这种交流就是消息传递。当我们收到一条消息(短信或电子邮件)时,我们所有的时间都花在了阅读和回复上。此外,我们 通常不会 等待答案并在此期间处理其他任务 。毫无疑问,在这种情况下,工作得到了优化并且可以有效地利用剩余的时间。看看下面的图:

笔记

要了解有关术语的更多信息,请参阅以下链接:

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

图 1.2。非阻塞消息通信

一般来说,为了在分布式系统中的服务之间进行通信时实现高效的资源利用, 我们必须 拥抱 消息驱动的通信原则。服务之间的整体交互可以描述如下——每个元素等待消息的到达并对它们做出反应,否则处于休眠状态,反之亦然,组件应该能够以非阻塞方式发送消息。此外,这种通信方法通过启用位置透明性来提高系统可扩展性。当我们向收件人发送电子邮件时,我们关心目标地址的正确性。然后邮件服务器负责将该电子邮件传送到收件人的可用设备之一。这让我们不必担心特定的设备,让收件人可以使用任意数量的设备。此外,它提高了容错能力,因为其中一个设备的故障不会阻止收件人阅读来自另一台设备的电子邮件。

实现消息驱动通信的方法之一是使用 消息代理。在这种情况下,通过监控消息队列,系统能够控制负载管理弹性。此外,消息通信提供了清晰的流量控制,简化了整体设计。我们不会在本章中详细介绍这一点,因为我们将在 第 8 章使用 Cloud Streams 进行扩展。

笔记

lying dormant这句话取自以下原始文档,旨在强调消息驱动的通信:https://www.reactivemanifesto.org/glossary#Message-Driven

通过接受所有前面的陈述,我们将获得反应式系统的基本原理。如下图所示:

 

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

图 1.3。反应式宣言

正如我们从图中可以注意到的那样,使用分布式系统实现的任何业务的 primary 值是响应能力。实现响应式系统意味着遵循弹性和弹性等基本技术。最后,实现响应式、弹性和弹性系统的基本方法之一是采用消息驱动的通信。此外,按照这些原则构建的系统具有高度可维护性 和可扩展性,因为系统中的所有组件都是独立且适当隔离的。

笔记

我们不会深入研究 Reactive Manifesto 中定义的所有概念,但强烈建议重新访问以下链接中提供的词汇表:https://www.reactivemanifesto.org/glossary

所有这些概念都不是新概念,并且已经在反应式宣言中定义,这是描述反应式系统概念的词汇表。创建此宣言是为了确保企业和开发人员对传统概念有相同的理解。需要强调的是,反应式系统反应式宣言与架构有关,这可能适用于大型分布式应用程序或小型单节点应用程序。

笔记

反应式宣言(https://www.reactivemanifesto.org)的重要性 是由 Lightbend 的创始人兼首席技术官 Jonas Bonér 在以下链接中解释:https://www.lightbend.com/blog/why_do_we_need_a_reactive_manifesto%3F

反应性用例


在上一节中,我们了解了反应性的重要性反应性系统的基本原理 我们也明白了为什么消息驱动的通信是反应式生态系统的重要组成部分。尽管如此,为了巩固我们所学到的知识,有必要接触其应用的真实示例。首先,响应式系统是关于架构的,它可以应用于任何地方。它可以用于简单的网站、大型企业解决方案,甚至可以用于快速流式传输或大数据系统。但是让我们从最简单的开始——考虑我们在上一节中已经看到的网络商店的例子。在本节中,我们将介绍可能有助于实现反应式系统的设计改进和更改。下图帮助我们熟悉所提出解决方案的整体架构:

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

图 1.4。商店应用架构示例

上图扩展了允许实现反应式系统的有用实践列表。在这里,我们通过应用现代微服务模式改进了我们的小型网络商店。在这种情况下,我们使用 API 网关模式来实现位置透明度。它提供了对特定资源的标识,而无需了解负责处理请求的特定服务。

笔记

但是,这意味着客户端至少应该知道资源名称。一旦 API 网关接收到作为请求 URI 一部分的服务名称,它就可以通过询问注册服务来解析特定的服务地址。

反过来,保持可用服务信息最新的责任是使用服务注册模式实现的,并在客户端发现模式的支持下实现。需要注意的是,在前面的示例中,服务网关和服务注册中心安装在同一台机器上,这在小型分布式系统的情况下可能很有用。此外,系统的高响应性是通过将复制应用于服务来实现的。另一方面,通过使用 Apache Kafka 和独立的支付代理服务(重试 N 次 Diagram 1.4中的描述,它负责在外部系统不可用的情况下重新支付。 另外,我们使用数据库复制以在其中一个副本中断的情况下保持弹性。为了保持响应,我们会立即返回有关已接受订单的响应,并异步处理并将用户付款发送到支付服务。稍后将通过支持的渠道之一(例如通过电子邮件)发送最终通知。最后,该示例仅描述了系统的一部分,在实际部署中,整体图可能更广泛,并引入了更具体的技术来实现反应式系统。

笔记

请注意,我们将在 第 8 章使用 Cloud Streams 进行扩展。

要熟悉 API Gateway、Service Registry 和其他构建分布式系统的模式,请点击以下链接:http://microservices.io/patterns

 

除了看似非常复杂的普通小型网络商店示例之外,让我们考虑另一个适合使用反应式系统方法的复杂领域。 分析是一个更复杂但令人兴奋的例子。术语分析 意味着能够处理大量数量数据的系统,在运行时处理它,让用户了解实时统计信息等。假设我们正在设计一个基于蜂窝基站数据监控电信网络的系统。根据最新的手机信号塔数量统计报告,2016 年美国有 308,334 个活跃站点。

笔记

美国蜂窝基站数量统计报告 可通过以下链接获得: https://www.statista.com/statistics/185854/mo​​nthly-number-of-cell-sites-in-the-united-states-since-june- 1986/

不幸的是,我们可以想象 这么多蜂窝站点产生的实际负载。但是,我们可以同意,处理如此大量的数据并提供对电信网络状态、质量和流量的实时监控是一项挑战。

为了设计这个系统,我们可以采用一种称为流式传输的高效架构技术。下图描述了这样一个流系统的抽象设计:

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

图 1.5。分析实时系统架构示例

从该图中可以看出,流式架构是关于数据处理和转换流的构建。一般来说,这样的系统的特点是低延迟和高吞吐量。反过来,响应或简单地提供电信网络状态的分析更新的能力因此至关重要。因此,要构建这样一个高可用的系统,我们必须依赖基本原则,正如 Reactive Manifesto 中提到的那样。例如,可以通过启用背压支持来实现弹性。背压是指处理阶段之间的工作负载管理的复杂机制,以确保我们不会压倒另一个。有效的工作负载管理可以通过使用可靠消息代理上的消息驱动通信来实现,该消息代理可以在内部保存消息并按需发送消息。

笔记

请注意,处理背压的其他技术将在 第 3 章中介绍/a>,反应式流 - 新流的标准

此外,通过适当地扩展系统的每个组件,我们将能够弹性地扩展或减少系统吞吐量。

笔记

要了解有关术语的更多信息,请参阅以下链接:Backpressure: https: //www.reactivemanifesto.org/glossary#Back-Pressure

在现实场景中,data的流可能是批量处理的持久化数据库,也可能是部分处理-时间通过应用窗口或机器学习技术。尽管如此,反应式宣言提供的所有基本原则在这里都是有效的,无论整个领域或商业理念如何。 

总而言之,有很多不同的领域可以应用构建反应式系统的基本原则。反应式系统的应用领域不限于前面的示例和领域,因为所有这些原则都可以应用于构建几乎任何种类 面向 提供 用户 有效的交互式反馈的分布式系统。

尽管如此,在下一节中,我们将介绍将 Spring 框架迁移到响应性的原因。

为什么选择反应弹簧?


在上一节中,我们查看了一些有趣的示例,其中反应式系统方法大放异彩。我们还扩展了用法弹性和弹性等基本原理,以及基于微服务的系统的示例 通常 用于实现反应式系统。

这让我们了解了架构的观点,但对实现却一无所知。然而,重要的是要强调反应系统的复杂性,而构建这样的系统是一个挑战。为了轻松创建反应式系统,我们必须首先分析能够构建此类事物的框架,然后选择其中一个。选择框架的最流行方法之一是分析其可用功能、相关性和社区。

在 JVM 世界中,用于构建反应式系统的最广为人知的框架是 Akka  和 Vert.x 生态系统。

一方面,Akka 是一个流行的框架,拥有大量的特性和庞大的社区。然而,在一开始 Akka 是作为 Scala 生态系统的一部分构建的,并且在很长一段时间内,它仅在用 Scala 编写的解决方案中展示了它的强大功能。尽管 Scala 是一种基于 JVM 的语言,但它与 Java 明显不同。 几年前,Akka 提供了对 Java 的直接支持,但由于某种原因,它在 Java 世界中并没有那么流行 它在斯卡拉。

另一方面,还有 Vert.x 框架,它也是构建 高效反应系统的强大解决方案。 Vert.x 被设计为在 Java 虚拟机上运行的 Node.js 的非阻塞、事件驱动的替代方案。然而,Vert.x 仅在几年前才开始具有竞争力,在过去的 15 年中,用于灵活健壮应用程序开发的框架市场 Spring 框架。 

笔记

要获取有关 Java 工具领域的更多信息,请点击以下链接:https://www.quora.com/Is-it-worth-learning-Java-Spring-MVC-as- of-March-2016/answer/Krishna-Srinivasan-6?srid=xCnf

Spring 框架为使用开发人员友好的编程模型构建 Web 应用程序提供了广泛的可能性。然而,长期以来,它在构建强大的反应系统方面存在一些局限性。

服务级别的反应性

幸运的是,对响应式系统不断增长的需求引发了一个名为 Spring Cloud 的新 Spring 项目的创建。 Spring Cloud 框架是解决特定问题并简化分布式系统构建的项目的基础。因此,Spring 框架生态系统可能与我们构建反应式系统相关。

笔记

要了解有关该项目的基本功能、组件和特性的更多信息,请单击以下链接:http://projects.spring.io/spring-cloud/

我们将跳过本章中 Spring Cloud Framework 功能的详细信息,并介绍有助于开发响应式系统的最重要部分第 8 章使用 Cloud Streams 扩展。 尽管如此,应该注意的是,这样的解决方案可以用最少的努力构建一个健壮的、反应式的微服务系统。

然而,整体设计只是构建整个反应系统的一个要素。从优秀的反应式宣言中可以看出:

“大型系统由较小的系统组成,因此取决于其组成部分的反应性属性。这意味着反应性系统应用设计原则,因此这些属性适用于所有规模级别,使它们能够组成”。

因此,在组件级别提供响应式设计和实现也很重要。在这种情况下,术语设计原则是指组件之间的关系,例如,用于组合元素的编程技术。用 Java 编写代码最流行的传统技术命令式编程

 

要了解命令式编程是否遵循响应式系统设计原则,让我们考虑下图:

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

图 1.6。组件关系的 UML Schema

在这里,我们在网络商店应用程序中有两个组件。在这种情况下,OrdersService 调用ShoppingCardService同时处理用户请求。假设在后台 ShoppingCardService 执行长时间运行的 I/O 操作,例如 HTTP 请求或数据库查询。为了理解命令式编程的缺点,让我们考虑以下组件之间交互的最常见实现的示例:

interface ShoppingCardService {                                    // (1)
   Output calculate(Input value);                                  //
}                                                                  //

class OrdersService {                                              // (2)
   private final ShoppingCardService scService;                    //
                                                                   //
   void process() {                                                //
      Input input = ...;                                           //
      Output output = scService.calculate(input);                  // (2.1)
      ...                                                          // (2.2)
   }                                                               //
}                                                                  //

 

上述代码解释如下:

  1. 这是 ShoppingCardService 接口声明。这对应于前面提到的类图,只有一个calculate 方法, 接受一个参数,处理后返回响应。
  2. 这是 OrderService 声明。在这里,在 (2.1) 点,我们同步调用  ShoppingCardService并在其执行后立即接收结果。 Point (2.2) 隐藏了负责结果处理的其余代码。
  3. 反过来,在这种情况下 我们的服务在时间上是紧密耦合的,或者只是 OrderService的执行 与 购物卡服务。不幸的是,使用这种技术,当 ShoppingCardService 处于处理阶段时,我们无法继续执行任何其他操作。

从前面的代码我们可以了解到,在Java世界中, scService.calculate(input) 的执行阻塞了 线程< /code> 处理 OrdersService 逻辑的地方.因此,要在 OrderService 中运行单独的独立处理,我们必须分配一个额外的 Thread。正如我们将在本文中看到的那样章,分配一个额外的线程 可能是浪费。因此, 从反应系统的角度来看,这种系统行为是不可接受的。

笔记

直接阻塞通信与消息驱动原则相矛盾,后者明确为我们提供了非阻塞通信。有关这方面的更多信息,请参阅以下内容:https://www.reactivemanifesto.org/ #消息驱动

尽管如此,在 Java 中,可以通过应用回调技术来解决这个问题。跨组件通信:

interface ShoppingCardService {                                    // (1)
   void calculate(Input value, Consumer<Output> c);                //
}                                                                  //

class OrdersService {                                              // (2)
   private final ShoppingCardService scService;                    // 
                                                                   //                                                
   void process() {                                                //
      Input input = ...;                                           //
      scService.calculate(input, output -> {                       // (2.1)
...                                                       // (2.2)
      });                                                          //
   }                                                               //
}                                                                  //

前面代码中的每个点都在以下编号列表中进行了解释:

  1. 前面的代码是 ShoppingCardService 接口声明。在这种情况下, calculate 方法 接受两个参数并返回一个空值。  这意味着从设计的角度来看,调用者可能会立即从等待中释放 并将结果发送给给定的Consumer<>回调。
  2. 这是 OrderService 声明。在这里,在 (2.1) 点我们异步调用  ShoppingCardService 并继续处理。反过来,当 ShoppingCardService 执行回调函数我们就可以进行实际结果处理(2.2)

现在,OrdersService 传递函数回调以在操作结束时做出反应。这包含了这样一个事实:OrdersService 现在 与ShoppingCardService分离 第一个可以通过ShoppingCardService#calculate的 实现 函数回调调用给定函数的方法可以是同步的或异步的:

class SyncShoppingCardService implements ShoppingCardService {     // (1)
   public void calculate(Input value, Consumer<Output> c) {        //
      Output result = new Output();                                //
      c.accept(result);                                            // (1.1)
   }                                                               //
}                                                                  //

class AsyncShoppingCardService implements ShoppingCardService{    // (2)
   public void calculate(Input value, Consumer<Output> c) {        //
      new Thread(() -> {                                           // (2.1)
         Output result = template.getForObject(...);               // (2.2) 
         ...                                                       //
         c.accept(result);                                        // (2.3)
      }).start();                                                  // (2.4)
   }                                                               //
}                                                                  //

 

 

前面代码中的每个点都在以下编号列表中进行了解释:

  1. 这一点是 SyncShoppingCardService 类声明。这个实现假设没有阻塞操作。由于我们没有 I/O 执行,因此可以通过将结果传递给回调函数 (1.1) 立即返回结果。
  2. 前面代码中的这一点是 AsyncShoppingCardService 类声明。在这种情况下,当我们有阻塞 I/O 时,如点 (2.2) 所述,我们可以将其包装在单独的 Thread (2.1)(2.4)。检索结果后, 它将被处理并传递给回调函数。

在该示例中,我们有 ShoppingCardService 的 sync 实现,它保持同步边界并且从API 角度来看没有任何好处。在异步情况下,我们实现了异步边界,一个请求将在单独的Thread中执行。 OrdersService被解耦从执行过程中,并将通过回调执行通知完成。

技术的优点是组件通过回调函数及时解耦。这意味着在调用 scService.calculate 方法后,我们将能够立即进行其他操作,而无需等待来自购物卡服务

缺点是回调需要开发者对多线程有很好的理解,避免共享数据修改和回调地狱的陷阱。

笔记

实际上,短语 callback hell 是与 JavaScript 相关的: http://callbackhell.com,但它也适用于 Java。

幸运的是,回调技术并不是唯一的选择。另一个是  java.util.concurrent.Future,对某些人来说度,隐藏执行行为并解耦组件:

interface ShoppingCardService {                                    // (1)
   Future<Output> calculate(Input value);                          // 
}                                                                  //

class OrdersService {                                              // (2)
   private final ShoppingCardService scService;                    //
                                                                   //
   void process() {                                                //
      Input input = ...;                                           //
      Future<Output> future = scService.calculate(input);          // (2.1)
      ...                                                          //
      Output output = future.get();                                // (2.2)
      ...                                                          //
   }                                                               //
}                                                                  //

编号点描述如下:

  1. 此时是 ShoppingCardService 接口声明。在这里, 计算 方法 接受一个参数并返回 FutureFuture 是一个类包装器,它允许我们检查是否有可用的结果或阻止获取它。
  2. 这是 OrderService 声明。在这里,在 (2.1) 点,我们异步调用 ShoppingCardService 并接收 未来 实例。反过来,我们能够在异步处理结果的同时继续处理。经过一些可能独立于 ShoppingCardService#calculation 的执行后,我们得到了结果。这个结果可能会以阻塞方式等待,也可能会立即返回结果(2.2)

正如我们在前面的代码中可能注意到的那样,使用 Future 类,我们实现了结果的延迟检索。在 Future 类的支持下,我们避免了回调地狱并将多线程复杂性隐藏在特定的 Future执行。无论如何,为了得到我们需要的结果,我们必须潜在地阻塞当前的 Thread 并与显着降低可扩展性的外部执行同步。

作为一项改进,Java 8 提供了CompletionStage 和CompletableFuture 作为CompletionStage 的直接实现 。反过来,这些类提供了类似 Promise 的 API,并且可以构建如下代码:

笔记

要了解有关 futurespromises 的更多信息,请查看 以下链接:https://en.wikipedia.org/wiki/Futures_and_promises

interface ShoppingCardService {                                    // (1)
   CompletionStage<Output> calculate(Input value);                 //
}                                                                  //

class OrdersService {                                              // (2)
   private final ComponentB componentB;                            //
   void process() {                                                //
      Input input = ...;                                           //
      componentB.calculate(input)                                  // (2.1)
                .thenApply(out1 -> { ... })                        // (2.2)
                .thenCombine(out2 -> { ... })                      //       
                .thenAccept(out3 -> { ... })                       //
   }                                                               //
}                                                                  //

上述代码描述如下:

  1. 此时,我们有了 ShoppingCardService 接口声明。在这种情况下,calculate方法接受一个参数并返回CompletionStageCompletionStage 是一个类似于 Future 的类包装器,但允许以函数式声明方式处理返回的结果。
  2. 这是一个 OrderService 声明。在这里,在 (2.1) 点我们异步调用  ShoppingCardService 并接收 CompletionStage 立即作为执行的结果。  CompletionStage的整体行为 类似于Future,但CompletionStage< /code> 提供了一个流畅的 API,使得编写诸如 thenAccept thenCombine等方法成为可能。这些定义了对结果的转换操作和定义最终消费者的thenAccept,以处理转换后的结果。

有了 CompletionStage的支持,我们可以编写 函数式和 声明式的代码,看起来干净并且异步处理结果。此外,我们可以省略等待的 结果 并提供一个函数来处理可用的结果。此外,之前的所有技术都受到 Spring 团队的重视,并且已经在框架内的大多数项目中实现。尽管 CompletionStage 为编写高效且可读的代码提供了更好的可能性,但不幸的是,这里仍然存在一些缺失点。例如,Spring 4 MVC 很长一段时间都不支持CompletionStage 为此,它提供了自己的ListenableFuture。这是因为 Spring 4 旨在与较旧的 Java 版本兼容。让我们概述一下 AsyncRestTemplate 的用法,以了解如何使用 Spring 的ListenableFuture。以下代码展示了我们如何将 ListenableFutureAsyncRestTemplate 一起使用:

AsyncRestTemplate template = new AsyncRestTemplate(); 
SuccessCallback onSuccess = r -> { ... }; 
FailureCallback onFailure = e -> { ... }; 
ListenableFuture<?> response = template.getForEntity(
   "http://example.com/api/examples", 
   ExamplesCollection.class 
);
response.addCallback(onSuccess, onFailure);

 

前面的代码显示了处理异步调用的回调样式。从本质上讲,这种通信方法是一种肮脏的黑客攻击,Spring Framework 将阻塞网络调用包装在一个单独的线程中。此外,Spring MVC 依赖于 Servlet API,它要求所有实现都使用每请求线程模型。

笔记

随着 Spring Framework 5 和新的 Reactive WebClient 的发布,很多事情都发生了变化,所以在 WebClient 的支持下,所有的东西都可以跨- 服务通信不再阻塞。此外,Servlet 3.0 引入了异步客户端-服务器通信,Servlet 3.1 允许对 I/O 进行非阻塞写入,并且总体而言,Servlet 3 API 的新异步非阻塞特性很好地集成到 Spring MVC 中。然而,唯一的问题是 Spring MVC 没有提供一个开箱即用的异步非阻塞客户端,它否定了改进的 servlet 带来的所有好处。

这个模型非常不理想。要理解为什么这种技术效率低下,我们必须重新审视多线程的成本。一方面,多线程本质上是一种复杂的技术。当我们使用多线程时,我们必须考虑很多事情,例如从不同线程访问共享内存、同步、错误处理等等。反过来,Java 中的多线程设计假设几个线程可以共享一个 CPU 来同时运行它们的任务。 CPU 时间将在多个线程之间共享这一事实引入了 上下文切换 的概念。这意味着稍后要恢复线程,需要保存和加载寄存器、内存映射和其他相关元素,这些元素通常是计算密集型操作。因此,具有大量活动线程和少量 CPU 的应用程序将是低效的。

笔记

要了解有关上下文切换成本的更多信息,请访问以下链接:https: //en.wikipedia.org/wiki/Context_switch#Cost

反过来,典型的 Java 线程在内存消耗方面也有其开销。 64 位 Java VM 上线程的典型堆栈大小为 1,024 KB。一方面,尝试在每个连接模型的线程中处理约 6,4000 个并发请求可能会导致大约 64 GB 的已用内存。从业务角度来看,这可能代价高昂,从应用程序的角度来看,这可能很关键。另一方面,通过切换到大小有限且为请求预先配置队列的传统线程池,客户端等待响应的时间过长,可靠性较差,增加了平均响应超时时间,最终可能导致应用程序无响应。

 

为此,Reactive Manifesto 建议使用非阻塞操作,这是 Spring 生态系统中的一个遗漏。另一方面,与Netty等反应式服务器没有很好的集成,解决了上下文切换的问题。

笔记

要获取有关平均连接数的来源信息,请参阅以下链接:https://stackoverflow.com/questions/2332741/what-is-the-theoretical-maximum-开放 tcp 连接数,即现代 lin/2332756#2332756

术语 thread 指的是为线程对象分配的内存和为线程堆栈分配的内存。有关更多信息,请参见下一个链接:

http: //xmlandmore.blogspot.com/2014/09/jdk-8-thread-stack-size-tuning.html?m=1

需要注意的是,异步处理不仅限于简单的请求-响应模式,有时我们必须处理不定式数据流,以具有背压支持的对齐转换流的方式处理它:

读书笔记《hands-on-reactive-programming-in-spring-5》为什么选择反应弹簧

图 1.7。反应式管道示例

处理此类情况的方法之一是通过反应式编程,它包含通过链接转换阶段的异步事件处理技术。因此,反应式编程是一种很好的技术,它符合反应式系统的设计要求。我们将在接下来的章节中介绍应用响应式编程来构建响应式系统的价值。

不幸的是,响应式编程 技术 没有很好地集成到Spring 框架中。这对构建现代应用程序造成了另一个限制,并降低了框架的竞争力。因此,在围绕响应式系统和响应式编程的不断增长的炒作中,所有提到的差距只会增加对框架内显着改进的需求。最后,通过在各个层面上增加对 Reactivity 的支持,并为开发人员提供了一个强大的响应式系统开发工具,这极大地刺激了 Sp​​ring Framework 的改进。它的关键开发人员决定实施新模块,以展示 Spring Framework 作为反应式系统基础的全部力量。

 

 

概括


在本章中,我们强调了当今经常出现的对具有成本效益的 IT 解决方案的要求。我们描述了为什么以及如何像亚马逊这样的大公司未能强制旧的架构模式在当前基于云的分布式环境中顺利运行。

我们还确定了对新架构模式和编程技术的需求,以满足对便捷、高效和智能数字服务不断增长的需求。通过响应式宣言,我们解构和理解了术语响应性,并描述了弹性、弹性和消息驱动方法为何以及如何帮助实现响应性,这可能是数字时代主要的非功能性系统要求。当然,我们给出了反应式系统大放异彩并轻松让企业 实现其目标的示例。

在本章中,我们强调了作为架构模式的反应式系统和作为编程技术的反应式编程之间的明显区别。我们描述了这两种类型的反应性如何以及为什么能够很好地协同工作,并使我们能够创建高效的顽固 IT 解决方案。

要深入了解 Reactive Spring 5,我们需要对反应式编程基础有深入的了解,学习决定该技术的基本概念和模式。因此,在下一章中,我们将学习反应式编程的基础知识、它的历史以及 Java 世界中反应式编程的现状。