1.什么是反应式编程
Reactive Programming
一种以异步处理数据流为中心思想的编程范式,这个范式存在已久,不是新概念,就像面向过程、面向对象编程、函数式编程等范式。
对比一下,Reactive streams指的是一套规范,对于Java开发者来讲,Reactive Streams就是一套API,使我们可以进行Reactive programming。
Reactive模型最核心的是线程和消息管道。线程用于侦听事件,消息管道用于线程之间通信不同的消息。
2.理论基础
2.1 反应式宣言
Reactive Manifesto。一切反应式概念的根源理论基础。
https://www.reactivemanifesto.org/zh-CN
描述了反应式系统(reactive systems)应该具备的四个关键属性:Responsive(灵敏的)、Resilient(可故障恢复的)、Elastic(可伸缩的)、Message Driven(消息驱动的)。
- Responsive(灵敏的):只要有可能,系统就会及时响应。灵敏性是系统可用性的基石,除此之外,灵敏性也意味着系统的问题可以被快速地探测和解决。具有灵敏性的系统关注做出快速和一致的响应,提供可靠和一致的服务质量。
- Resilient(可故障恢复的):在出现故障时,系统仍然可以保持响应。一个不具可恢复性的系统一旦出现故障,就会变得无法正常响应。可恢复性可以通过复制、围控、隔离和委派等方式实现。在可恢复性的系统中,故障被包含在每个组件中,各组件之间相互隔离,从而允许系统的某些部分出故障并且在不连累整个系统的前提下进行恢复。
- Elastic(可伸缩的):在不同的工作负载下,系统保持响应。系统可以根据输入的工作负载,动态地增加或减少系统使用的资源。这意味着系统在设计上可以通过分片、复制等途径来动态申请系统资源并进行负载均衡,从而去中心化,避免节点瓶颈。
- Message Driven(消息驱动的):反应式系统依赖异步消息传递机制,从而在组件之间建立边界,这些边界可以保证组件之间的松耦合、隔离性、位置透明性,还提供了以消息的形式把故障委派出去的手段。
- Failures at messages:在 Reactive 编程中,我们通常需要处理流式的信息,我们最不希望看到的是突然抛出一个异常,然后处理过程终止了。理想的解决办法是我们记下这个错误,然后开始执行某种重试或恢复的逻辑。在 Reactive Streams 中,异常是一等公民,异常不会被粗鲁地抛出,错误处理是正式建立在 Reactive Streams API 规范之内的。
- Back-pressure:中文一般翻译成“背压”、“回压”,意思是当消费端的消费能力跟不上生产端的生产速度时,消息流下游的消费方对上游的生产方说:“我喝饱了,请你慢点”。在 Reactive 的世界里,我们希望下游的消费方可以有某种机制按需请求一定数量的消息来消费(这类似消息队列中的 pull 的概念)。而不是上游把大量的消息一股脑灌给下游消费方,然后阻塞式等待,throttling(节流) is done programmatically rather than blocking threads。
- Non-blocking:数据处理以一种非阻塞的方式被处理,线程不会因为等待别的处理过程而卡住。这里可以对比有着 非阻塞事件循环 的Node.js Server(如一条高速公路)和传统的Java多线程服务(如拥有红绿灯的十字路口)
2.2 Imperative vs Reactive
指令式编程 vs 反应式(响应式)编程
2.2.1 概念
from 维基百科
- 指令式编程:是一种描述电脑所需作出的行为的编程典范。几乎所有电脑的硬件工作都是指令式的;几乎所有电脑的硬件都是设计来运行机器代码,使用指令式的风格来写的。较高端的指令式编程语言使用变量和更复杂的语句,但仍依从相同的典范。菜谱和行动清单,虽非计算机程序,但与指令式编程有相似的风格:每步都是指令,有形的世界控制情况。因为指令式编程的基础观念,不但概念上比较熟悉,而且较容易具体表现于硬件,所以大部分的编程语言都是指令式的。
- 反应式编程:一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。最初是为了简化交互式用户界面的创建和实时系统动画的绘制而提出来的一种方法,但它本质上是一种通用的编程范式。
2.2.2 差异比较
两种不同的编程范式
- 指令式编程:最常见的编程模式,平时写的代码基本都属于指令式编程。代码被按顺序一行一行执行,“按顺序”同时只可以使用各种条件、循环或者方法调用来让编译器按作者意图顺序执行代码。
- 反应式编程:基于异步数据流的编程,可以动态的创建、改变和组合。(Reactive programming is programming with asynchronous data streams that can be created, changed or combined on the go)
举个代码的例子
举个现实的例子
以汽车?来举例,
- 指令式编程中,数组就像停车场一样,存放了不同的汽车,我们可以检查停车场,看到所有停放的汽车,依次记录当前停在停车场的所有汽车。
- 反应式编程中,数据流Stream就像一条街道,汽车会出现在街道但会立即驶过,他们并没有停在那里。当我们观察街道时,只能看到汽车依次驶过,所以“记录汽车”在这个场景下是指持续观察一条定义好的道路。
3.理论实践
3.1 Reactive streams api
java8 时代的开源jar包,在java9时代被正式引入java api。
官网:https://www.reactive-streams.org/
github:https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.3/README.md
简介
Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. This encompasses efforts aimed at runtime environments (JVM and JavaScript) as well as network protocols.
- 反应流(Reactive Streams)是一个对于 异步流处理且伴随非阻塞回压机制 而提供的标准。这个标准服务于运行时环境(JVM和Javascript)以及网络协议。
目标、设计与适用范围
Handling streams of data—especially “live” data whose volume is not predetermined—requires special care in an asynchronous system. The most prominent issue is that resource consumption needs to be carefully controlled such that a fast data source does not overwhelm the stream destination. Asynchrony is needed in order to enable the parallel use of computing resources, on collaborating network hosts or multiple CPU cores within a single machine.
- 处理流式数据——尤其是量无法预估的实时数据时,在一个异步系统内要格外小心。
- 最重要的问题是:资源的消耗需要被小心的控制,从而使一个快速发送的数据源不会淹没下游。
- 需要异步的原因,是为了并行的使用资源,在多个主机或同一主机多个CPU核的场景下。
The main goal of Reactive Streams is to govern the exchange of stream data across an asynchronous boundary – think passing elements on to another thread or thread-pool — while ensuring that the receiving side is not forced to buffer arbitrary amounts of data. In other words, backpressure is an integral part of this model in order to allow the queues which mediate between threads to be bounded.
- 反应流的主要目标:控制横穿异步边界的流数据交换(例如从一个线程池向另一个线程池传递数据),同时要确保接收端不被强迫缓冲任意数量的大量数据。
- 换句话说,后压(backpressure)是这个模型的一部分,目的是允许队列在被界定的线程之间进行调节。
In summary, Reactive Streams is a standard and specification for Stream-oriented libraries for the JVM that
- process a potentially unbounded number of elements
- in sequence,
- asynchronously passing elements between components,
- with mandatory non-blocking backpressure.
总之,反应流是jvm上面向流的库的一个标准和规范:
- 处理一个潜在无限元素的数目
- 依次处理
- 异步地在组件之间传递元素
- 必须强制有非阻塞后压
3.2 Java9 Flow
jdk9中集成,由reactive-stream-jvm发展而来。
The interfaces available in JDK9’s java.util.concurrent.Flow, are 1:1 semantically equivalent to their respective Reactive Streams counterparts. This means that there will be a migratory period, while libraries move to adopt the new types in the JDK, however this period is expected to be short - due to the full semantic equivalence of the libraries, as well as the Reactive Streams <-> Flow adapter library as well as a TCK compatible directly with the JDK Flow types.
jdk9中的java.util.concurrent.Flow是对Reactive Streams的完全对等实现,除了类名不同以外其他部分都可以做一对一迁移。
Jdk9的Reactive Stream API(java.util.concurrent.Flow)只是一套接口,约定了Reactive编程的一套规范,并没有具体的实现。而实现了这个接口的产品有:RxJava、Reactor、Akka等,而Spring WebFlux中集成的是Reactor3.0。
3.3 Reactive Extensions(Rx)
目前 Java 平台上主流的反应式库有两个,分别是 Netflix 维护的 RxJava 和 Pivotal 维护的 Reactor。RxJava 是 Java 平台反应式编程的鼻祖。反应式流规范在很大程度上借鉴了 RxJava 的理念。
由于 RxJava 的产生早于反应式流规范,与规范的兼容性并不是特别好。
ReactiveX最初被微软应用在.NET上,而后慢慢的在衍生出了各种不同语言的实现,诸如RxSwift/RxJava/RxJS。
Rx提供了一种新的组织和协调异步事件的方式,极大的简化了代码的编写。Rx最显著的特性是使用可观察集合(Observable Collection)来达到集成异步(composing asynchronous)和基于事件(event-based)的编程的效果。
RxJava:https://github.com/ReactiveX/RxJava
以RxJS角度分析Reactive Programming:https://blog.techbridge.cc/2016/05/28/reactive-programming-intro-by-rxjs/
3.4 Reactor
一种反应式编程框架。
有两种模型,Flux 和 Mono,提供了非阻塞、支持回压机制的异步流处理能力。当数据生成缓慢时,整个流自然进入推送模式;而当生产高峰来临数据生产速度加快时,整个流又进入了拉取模式。Flux 可以触发 0 到多个事件,用于异步地处理流式的信息;Mono 至多可以触发一个事件,通常用于在异步任务完成时发出通知。Flux 和 Mono 之间可以相互转换。对一个 Flux 序列进行计数操作,得到的结果是一个 Mono对象,把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。
3.5 Spring5.0 WebFlux
Spring5.0的新模块,基于Reactor框架。包含了反应式HTTP和WebSocket的支持。上层编程模型同时支持SpringMVC中基于Java注解方式以及Java8的lambda表达式的函数式编程模型。只在编码方式不同,运行时效果相同都在反应式底层架构之上。
webflux的关键是自己编写的代码里面返回流(Flux/Mono),spring框架来负责处理订阅。
与传统 Spring MVC 的区别在于,WebFlux 的请求和响应使用的都是 Flux 或 Mono 对象。一般的 REST API 使用 Mono 来表示请求和响应对象;服务器推送事件使用 Flux 来表示从服务器端推送的事件流;WebSocket 则使用 Flux 来表示客户端和服务器之间的双向数据传递。
为了最大程度的发挥反应式流和负压的作用,WebFlux 应用的各个部分都应该是支持反应式的,也就是说各个部分都应该是异步非阻塞的。要做到这一点,需要其他的库提供支持,主要是与外部系统和服务整合的部分。
比如在数据访问层,可以通过 Spring Data 的反应式支持来访问不同类型的数据源。当然这也需要底层驱动的支持。越来越多的数据源驱动已经提供了对反应式流规范的支持,还有很多开源库可以使用。
3.6 Akka
https://doc.akka.io/docs/akka/current/typed/guide/introduction.html
一个基于反应式编程理念的全异步、高并发、可容错的事件驱动编程框架,构建于JVM上,支持java和scala开发。
Actors 为你提供:
- 对并发/并行程序的简单的、高级别的抽象。
- 异步、非阻塞、高性能的事件驱动编程模型。
- 非常轻量的事件驱动处理(1G内存可容纳数百万个actors)。
容错性
- 使用“let-it-crash”语义的监控层次体系。
- 监控层次体系可以跨越多个JVM,从而提供真正的容错系统。
- 非常适合编写永不停机、自愈合的高容错系统。
位置透明性
Akka的所有元素都为分布式环境而设计:所有actor只通过发送消息进行交互,所有操作都是异步的。
持久性
actor接收到的消息可以选择性的被持久化,并在actor启动或重启的时候重放。这使得actor能够恢复其状态,即使是在JVM崩溃或正在迁移到另外节点的情况下。
4.优点与缺点
了解了反应式编程原理,那使用反应式编程设计的优势和劣势是什么呢?
StackOverflow上有相关话题讨论
https://stackoverflow.com/questions/42062199/reactive-programming-advantages-disadvantages
4.1 优点
- 避免"callback hell"(callback hell是指在js中所有异步都需要靠回调实现,当异步过多时造成代码嵌套大量回调 http://callbackhell.com)
- 大幅简化异步和线程操作
- 简单的组合数据流操作
- 复杂的线程操作变得简单
- 代码变得更清晰可读
- 轻松地实现“回压”
4.2 缺点
- 流数据(Stream data)占据空间,系统中绝大多数时间存在大量流数据。
- 编程习惯需要适应,一切指令式编程中“调用”概念皆转换为“流”
- 宕机时内存中流数据丢失
4.3 性能提高
上个月《大型网站技术架构:核心原理与案例分析》的作者,目前腾讯云TVP李智慧刚在infoQ发表的文章
介绍了基于Akka开发的全异步反应式框架Flower https://github.com/zhihuili/Flower
文章中提供了理论依据以及相关性能优化测试数据。
Flower 支持异步数据库驱动,用户请求数据库的时候,将请求提交给异步数据库驱动,立刻就返回,不会阻塞当前线程,异步数据库访问连接远程的数据库,进行真正的数据库操作,得到结果以后,将结果以异步回调的方式发送给 Flower 的 Service 进行进一步的处理,这个时候依然不会有线程被阻塞。也就是说使用 Flower 开发的系统,在一个典型的 Web 应用中,几乎没有任何地方会被阻塞,所有的线程都可以被不断的复用,有限的线程就可以完成大量的并发用户请求,从而大大地提高了系统的吞吐能力和响应时间,同时,由于线程不会被阻塞,应用就不会因为并发量太大或者数据库处理缓慢而宕机,从而提高了系统的可用性。
全部异步化意味着更少线程切换、避免了线程阻塞,从而获得更好的执行性能。
简单的举个例子来说,以前一个线程要做A、B、C三个方法,但是C存在IO阻塞(如数据库操作),当C方法延时高时,会使系统整体在阻塞状态的线程增多,A、B方法也受影响。
但使用反应式编程概念,A、B、C三个方法都有各自的线程(或线程池)来处理,三个方法的触发使用数据流(也可称事件)串起来,则C的阻塞不会影响A、B的处理。