序言
大家好,我是比特桃。本文为《Spring 响应式编程》的读书笔记,响应式技术栈可以创建极其高效、易于获取且具有回弹性的端点,同时响应式可以容忍网络延迟,并以影响较小的方式处理故障。响应式微服务还可以隔离慢速事务并加速速度最快的事务。通过本书可以学到以下内容:
- 响应式编程基本原则和响应式流(Reactive Stream)规范;
- 使用 Spring 5 集成的 Project Reactor 响应式开发框架;
- 使用 Spring Webflux 构建响应式 RESTful 服务;
- 使用 Spring Data Reactive 构建响应式数据访问组件;
- 使用 Spring Cloud Stream Reactive 构建响应式消息通讯组件。
第一章 响应式Spring
首先了解一下响应式编程相关的名词:
- 弹性:可以横向扩展;
- 回弹:故障隔离,做到独立性;
- 消息驱动:不应该是阻塞的,即时响应。
响应式中,较为关键的是背压:主要用来支持回弹性,实现处理阶段之间工作负载管理的复杂机制,可以确保一个处理阶段不会压垮另一个。
在JVM领域,构建响应式系统最知名的两个框架:Akka 、Vert.x。而在在传统 Java 中,实现响应式的方式:
- 回调函数,但会带来回调地狱问题;
- JDK 8 CompletionStage.
Spring 4为了兼容旧 JDK 没有支持CompletionStage,在Spring 5中才支持该方法。同时,Servlet 3.0 引入了异步客户端-服务器通讯,3.1 支持I/O 非阻塞写入。但是Spring MVC 没有提供一个开箱即用的异步非阻塞客户端。为了陈旧包袱,彻底丧失响应式支持。也导致了 Spring为响应式单独开了一份技术栈。
第二章 Spring响应式编程基本概念
发布订阅模式,它可以被视为观察者模式的变体。在基于RxJava的实现中,但没有人监听时,温度传感器不会探测温度数据。这种行为是响应式编程具有主动订阅概念这一事实的自然结果。基于发布-主题的实现没有这样的属性,因此存在更多限制。
RxJava库是 Reactive Extensions(响应式扩展,也称为 ReactiveX)的Java虚拟机实现。Reactive Extensions 是一组工具,能用命令式语言处理数据流,无论该流是同步的还是异步的。ReactiveX通常被定义为观察者模式、迭代器模式和函数式编程的组合。
RXJava通过订阅者订阅可观察流,该流又反过来触发事件生成的异步过程。采用这种方法对可变对象有害,唯一合理的策略是采用不变性。不变性即函数式编程(functional programming)的核心原则之一。对象一旦被创建,就不会被更改。这样一条简单的规则可以防止在并行系统中可能出现的问题。
如果没有函数式编程,人们就必须创建许多匿名类或者内部类,这些类会污染应用程序。并且他们创建的样板代码多于有效代码。正由于它的不可变性,非常适合应用于并发编程。
历史
异步编程的历史最早追溯05年微软做的:大规模异步数据密集型互联网服务架构的编程模型。09年开源了Rx.NET,后来Netflix正面临大量互联网流量这一复杂难题,就开源了RxJava。并基于它开发了诸如:Hystrix、Ribbon、Zuul、RxNetty知名的库。但其实在响应式编程方向完全落实并成功做出巨大贡献的是:Node.js。
RxJava也并不是Java中唯一的解决方案,Vert.x也可以实现类似功能。Vert.x是一个事件驱动的开发框架,在设计上类似Node.js。它为异步编程提供了一个简单的并发模型和原始语义。
随着RxJava的成功,许多公司和开源项目都开始了激烈的竞争。由于各个库的行为总体上非常相似,但在实现中又略有不同。所以如果共用一些响应式库,可能会出现隐藏的bug。为了解决这些兼容性问题,出现了一个标准:响应式流(Reactive Streams)。
第三章 响应式流-新的标准
使用多个响应式库需要自己去编写适配器进行兼容,Spring 4的ListenableFuture和CompletionStage之间并没有直接的集成,而在Spring 5中才扩展了ListenableFuture的API名为Comple-table的方法来解决不兼容问题。核心问题在于没有一种方法能够使库提供商提供对齐的API,Vert.x、Ratpack和Retrofit都比较重视RxJava,并提供了支持。
在整个响应式环境演变的早期阶段,所有库的设计思想都是把数据从源头推送到订阅者。采用推模型的主要原因是它可以通过将请求量减少到最小值来优化整体处理时间。
这也就是为什么RxJava1.x及类似的开发库以推送数据为目的进行设计,这也是为什么流技术能称为分布式系统中组件之间重要的通讯技术。但如果生产者不关注消费者的吞吐能力,则会存在一下可能:
- 慢生产者和快消费者
- 快生产者和慢消费者
针对这种情况的解决方案是将未处理的元素收集到队列中。
- 无界队列:无限大小的队列;
- 有界丢弃队列:为避免内存溢出;
- 有界阻塞队列:支付订单阻塞;
通常,纯推模型中不受控制的语义可能导致许多我们不希望出现的情况。响应式宣言中提到使系统巧妙地响应负载的机制的重要性,及背压的重要性。
响应式流规范定义了4个主要的接口:Publisher、Subscriber、Subscription和Processor。
其中,Publisher和Observable、Subscriber和Observer基本相同;
在Subscriber中的onSubscribe方法中,引用了Subscription,这为控制元素的生产提供了基础。响应式流规范引入了request方法以扩展Publisher和Subscriber之间的交互能力。为了通知Publisher应该推送多少数据,Subscriber应该通过request方法发出关于所需数量的信号,并确保传入元素的数量不超过限制。
与纯推模型相反,该规范为我们提供了混合推拉模型,而此模型可以对背压进行合理控制。
Processor是Publisher和Subscriber的组合。它的目标是再Publisher和Subscriber之间添加一些处理阶段。
第四章 Project Reactor 响应式应用程序的基础
响应式流规范(reactivestreams)使得响应式库彼此兼容,并通过引入拉-推数据交换模型解决了背压问题。但它只是定义了规范,并不提供日常使用,Spring 实现了Project Reactor(简称Reactor)。
响应式流规范(reactivestreams)使得响应式库彼此兼容,并通过引入拉-推数据交换模型解决了背压问题。但它只是定义了规范,并不提供日常使用,Spring 实现了Project Reactor(简称Reactor)。Reactor1.x 版本包含了消息处理的最佳实践,例如Reactor模式(Reactor Pattern),以及函数式和响应式编程风格。
Reactor模式是一种行为模式,有助于异步事件响应和同步处理。这意味着所有事件都需要排队,并且事件的实际处理稍后由单独的现成负责执行。一个事件被分派给所有有关方面(事件处理程序)并进行同步处理。
Reactor 1.x与Spring框架很好地集成在一起,Reactor 1.x 与消息处理库一起提供了许多附加的组件,例如针对Netty的附加组件。Reactor 2首开响应式流之先河。将事件总线和流功能提取到单独的模块中,使得Reactor Streams库完全符合响应式流规范。Reactor API与Java Collections API 具有更好的集成性。Reactor的Streams API与RxJavaAPI更加相似,补充了在背压管理、线程调度和回弹性方面的支持。发送消息的Reactor对象被重命名为了EventBus。
RxJava和ProjectReactor的想法浓缩成了一个reactive-stream-commons的库,后来该库也成为了Reactor3.x。同时,Reactor3.x塑造了Spring 5框架的响应式变种(reactive metamorphosis)
Reactor库设计的目的就是为了在构建异步管道时避免回调地狱和深层嵌套代码。我们可以将响应式应用程序的处理的数据视为在装配线上的移动。Reactor即是传送带又是工作站。Reactor API只有订阅才会触发真实的数据流。该库常见的背压传播模式:
- 仅推送:当订阅者通过 subscription.request(Long.MAX_VALUE)请求有效无线数量额元素时。
- 仅拉取:当订阅者通过 subscription.request(1) 仅在收到前一个元素后请求下一个元素时。
- 拉-推(有时成为混合):当订阅者有实时控制需求,且发布者可以适应所提出的数据消费速度时。
Project Reactor是建立在响应式流规范之上的,org.reactivestreams:reactive-streams 是 Project Reactor 的唯一强制依赖。响应式流规范自定义了4个接口,即 Publisher、Subscriber、Subscription和Processor<T, R>。
ProjectReactor提供了Publisher接口的两种实现,即Flux和Mono。
Mono是Reactor的两种核心类型之一,另一个类型是Flux。 两者都实现了反应式流的Publisher接口。Flux代表具有零个、一个或者多个(可能是无限个)数据项的管道。它的公式为: onNext x 0…N [onError | onComplete]。例如:Flux.range(1, 5).repeat();
Mono是一种特殊的反应式类型,针对数据项不超过一个的场景,它进行了优化。公式表示为:onNext x 0…1 [onError | onComplete]。当应用程序API最多返回一个元素时,Mono很有用。因此,它可以替换comple-tableFuture,并提供相似的语义。CompletableFuture会立即开始处理,而Mono在订阅者出现之前什么都不做。Mono类型的好处只在于它不仅提供了大量的响应式操作符,还能够完美地结合到更大的响应式工作流程中。例子:当操作完成需要通知客户端的时候,也可以使用Mono,处理完成时发出 onComplete()信号,发生故障时返回onError()。在这种场景下,不会返回任何数据,而只是发出通知信号,而这个通知信号反过来可以用作进一步计算的触发器。
Flux和Mono是Reactor提供的最基础的构建块,而这两种反应式类型所提供的操作符则是组合使用它们以构建数据流动管线的黏合剂。Flux和Mono共有500多个操作,这些操作都可以大致归类为:
创建操作;
组合操作;
转换操作;
逻辑操作。
第五章 使用Spring Boot 2实现响应性
Project Reactor可以在没有Spring框架的基础上运行,如果结合Spring的依赖注入,将会变得更好。
09年Spring团队为了快速开发应用,使用了约定优于配置(Convention-over-configuration)的方法。12年发布了Spring boot,采用了无容器Web应用程序的理念和可执行的胖JAR(Fat JAR)技术。最关键的两个注解:@SpringBootApplication,用来运行IoC容器;@spring-boot-autoconfigure,自动配置一些“-starter-”后缀的组件。
Spring 5.x引入了对响应式流和响应式库的原生支持,其中响应式库包含了RxJava 1/2和Project Reactor3。
Servlet API 3.1的适配器提供了与WebMVC适配器不同的纯异步和非阻塞集成。当然,Spring WebMVC模块还支持Servlet API4.0,后者支持HTTP/2。
早期,Spring Data主要提供对底层存储区域的同步阻塞访问。幸好,第五代 Spring Data 提供了ReactiveCrudRepository接口,该接口暴露了ProjectReactor的响应式类型,以便于响应式工作流程无缝集成。
Spring Sessiong模块可以试用高效的抽象来进行会话管理,Spring Sessiong引入了ReactiveSessionRepository,它可以使用Reactor的Mono类型对存储的会话进行异步非阻塞访问。
旧的Spring Security使用ThreadLocal作为SecurityContext实例的存储方法,在单个Thread内执行的时候很有效,在任何时候,我们都可以访问存储在ThreadLocal中的SecurityContext。但是在异步通讯时,我们需要将ThreadLocal内容传输到另一个Thread。如今新一代Spring Security采用了Reactor上下文功能,以便在Flux或Mono流中传输安全上下文。
Netflix Zuul基于使用阻塞同步请求路由的ServletAPI,使处理请求无效并获得更好性能的唯一办法是调整底层服务器线程池。幸好Spring Cloud引入了新的Spring Cloud Gateway模块,该模块构建于Spring WebFlux之上,并在Project Reactor 3 的支持下提供异步和非阻塞路由。除了网关,Spring Cloud Streams还获得了Project Reactor的支持,并引入了更加细粒度的流模型。还有Spring Cloud Function和Spring Cloud Data Flow,这些可以构建自己的FaaS(Function as a service)。
Spring Actuator提供了与WebFlux的完全集成,并使用其异步、非阻塞变成模型,以便有效地暴露指标端点。Spring Cloud Sleuth模块也支持了Project Reactor的响应式编程,开箱即用的分布式跟踪。
第六章 WebFlux异步非阻塞通讯
从Spring框架在Web应用程序领域开始演进起,就将Spring Web模块与Java EE的 Servlet API进行集成的决定。Spring框架的整体基础设施都是围绕Servet API构建的,他们之间紧密耦合。例如,Spring Web MVC 整体以Front Controller模式为基础。该模式在Spring Web MVC中由org.springframework.web.servlet.DispatcherServlet 类实现,而该类简介扩展了javax.servlet.http.HttpServlet类。
整体设计依赖于底层Servlet容器,而该容器负责处理容器内所有映射Servlet。DispatchServlet作为一个集成点,用于集成灵活且高度可配置的Spring Web基础设施和繁重且复杂的Servlet API。HandlerMapping的可配置抽象有助于将最终的业务逻辑(如控制器和bean)与Servlet API分离。
尽管Servlet API支持异步、非阻塞通讯(从3.1版本开始),但SpringMVC模块的实现不仅存在很多缺陷,还不允许在整个请求生命周期中出现非阻塞操作。例如它没有开箱即用的非阻塞HTTP客户端,任何外部交互都可能导致阻塞的I/O调用。旧版的Spring中Web抽象的另一个劣势是,对于一个非Servlet服务器(如Netty)而言,重用Spring Web功能或编程模型没有灵活性。
WebFlux提供了开发轻量级应用,该特性是通过函数式路由映射和我们能够编写复杂的请求路由逻辑的内置API实现的。纯函数式路由的组合足以适应新的响应式编程方法。此外,还提供了WebClient的旧RestTemplate的响应式替代品。虽然WeboSocket在13年就引入了Spring,但目前仍然有一些阻塞操作,例如将数据写入I/O或从I/O读取数据仍然是阻塞操作。WebFlux模块为WebSocket引入了改进版本的基础设施,并且提供了客户端的支持。
对比WebFlux和WebMVC
以前,计算机是顺序的,大家习惯浏览简单静态的内容,系统的总体负载总是很低的。而现在Web用户数量达到了十几级,并且内容开始变成动态的甚至是实时的,而对吞吐量和延迟的要求已经发生了很大变化。要计算并行工作单元的数量如何改变延迟或吞吐量,可以用到利特尔定律(Little’s Law):N = X x R。系统或队列驻留的平均请求数(或同时处理的请求数)(N)等于吞吐量(或每秒用户数)(X)乘以平均响应时间或延迟时间(R);例如:系统平均响应时间R为0.2S,吞吐量X为每秒100个请求,那么它应该能够同时处理20个请求,或者并行处理20个用户。
传统的基于Servlet的Web框架,如Spring MVC,在本质上都是阻塞和多线程的,每个连接都会使用一个线程。在请求处理的时候,会在线程池中拉取一个工作者(worker)线程来对请求进行处理。同时,请求线程是阻塞的,直到工作者线程提示它已经完成为止。
这样带来的后果就是阻塞式Web框架在大量请求下无法有效地扩展。缓慢的工作者线程所带来的延迟会使情况变得更糟,因为它将花费更长的时间才能将工作者线程送回池中,准备处理另一个请求。在某些场景中,这种设计完全可以接受。事实上,在很大程度上这就是十多年来大多数Web应用程序的开发方式,但是时代在改变。
这些Web应用程序的客户端以前是偶尔浏览网站的人们,而现在这些人会频繁消费内容而且会使用与HTTP API协作的应用程序。如今,物联网(甚至不需要人类)产生了汽车、喷气式发动机和其他非传统的客户端,它们会持续地和Web API交换数据。随着消费Web应用的客户端越来越多,可扩展性比以往任何时候都更加重要。
异步的Web框架能够以更少的线程获得更高的可扩展性,通常它们只需要与CPU核心数量相同的线程。通过使用所谓的事件轮询(event looping)机制(如图11.1所示),这些框架能够用一个线程处理很多请求,这样每次连接的成本会更低。
右上角的方框表示另一种编程模型,它使用函数式编程范式来定义控制器,而不是使用注解。
尽管Spring WebFlux控制器通常会返回Mono和Flux,但是这并不意味着Spring MVC无法体验反应式类型的乐趣。如果你愿意,那么Spring MVC也可以返回Mono和Flux。
这里的区别在于,这些类型会如何被使用。Spring WebFlux是真正的反应式Web框架,允许在事件轮询中处理请求;而Spring MVC是基于Servlet的,依赖于多线程来处理多个请求。
Web MVC建立在阻塞I/O之上,这意味着处理每个传入请求的Thread可能被从I/O读取传入的消息所阻塞。
相比之下,WebFlux构建在非阻塞API之上,这意味着没有操作需要与I/O阻塞Thread进行交互。WebFlux能比Web MVC更有效地利用一个Thread。
WebFlux应用的场景:
- 微服务系统:典型微服务系统最显著的特点就是大量的I/O通讯,I/O的存在,尤其是阻塞式I/O,会降低整个系统延迟和吞吐量。
- 处理客户端连接速度慢的系统:如果客户端数量很多,那么系统崩溃的可能性会比较大。例如,黑客能很容易的通过使用拒绝服务(Denial-of-Service,DoS)攻击使我们的服务器不可用。相比之下,WebFlux使我们能在不阻塞工作线程的情况下接受连接。
- 流或实时系统:这些系统的特点是低延迟和高吞吐量,使用非阻塞通讯可以实现低延迟和高吞吐量。然而这种响应式框架有其自身的缺点,即使用通道和回调的复杂交互模型。但我们可以通过响应式库构建一个异步的非阻塞流而且还需要很小的开销。
WebFlux可用响应式Web服务器(Netty)和非阻塞的Undertow功能
第七章 响应式数据库访问
响应式应用程序中始终不鼓励阻塞I/O,Spring Data模块以响应式方式访问数据,即便选择的数据库没有提供响应式或异步驱动程序,我们仍然可以使用专用的线程池来构建一个围绕它的响应式应用程序。
Eric Evans《领域驱动设计:软件核心复杂性应对之道》为成功的为微服务架构定义了重要的理论基础,并使其成型。领域驱动设计(Domain-Driven Design,DDD),建立了一个通用的词汇表(即上下文、领域、模型和统一语言),根据DDD定义的单个边界上下文(bounded context)通常会被映射到单独的微服务中。
由于DDD非常关注业务核心域(core domain),尤其是用来表达、创建和检索域模型的工件,因此实体(Entity)、值对象(Value object)、聚合(Aggregate)、存储库(Repository)等对象在本章中将会被频繁提及。
在考虑DDD的应用程序实现期间,应将上述对象映射到应用程序持久化层。这种领域模型构成了逻辑和物理数据模型的基础。
未完待续……