阿里淘宝一直在推的响应式编程到底是个什么鬼?

随着这些年智能手机的发展和普及,我们的服务器端要应对日益增长的巨大流量。

从开发的角度来看,这就要求我们必须设计出高扩展性和高可用性的程序,以确保能够适应日益增长的请求所带来的压力。

而从使用者的角度来看,他们并不会关心后台到底是怎样的,而更关注App界面的易用性和是否美观,最重要的是每次操作能否得到快速的响应。

举一个例子,在CPU还是单核的年代,计算机上只运行一个程序就能够将CPU占用得满满当当。如果计算机上同时运行两个程序,用户就会觉得卡顿,此时CPU要通过上下文切换轮流处理这两个程序,而这个切换过程会消耗CPU资源,以及占用更多的内存资源。而在CPU是多核的时代,我们可以同时打开多个程序,还可以随意切换,并且没有丝毫停滞感。

与硬件端的发展相似,现在Web前端已经可以实现单页应用。举一个例子,在使用音乐App的场景下,点开一个歌单,这个获取歌单的过程可能由于多种原因而产生延时;与此同时,可以切换到这个App的评论区,查看评论将丝毫不影响后台正在进行的获取歌单的任务,等切换回来时可以立马展示获取到的歌单。

于是,对开发人员来说,有两种选择摆在面前。第一种是在从数据库中查找歌单任务完成之前,让用户一直等待,而不能进行其他操作;第二种是用户可以在查找歌单任务执行期间同时使用其他功能,而不是切换回来之后重新查找。

对于用户来说,第二种肯定是最佳选择,至此引出了我们的话题:基于异步的开发模式和传统的基于同步的开发模式。后者比较简单,不必多说,因此我们接下来要介绍的就是基于异步的开发模式,即异步编程模式。

异步编程模式

大家学Java这么久,应该都很清楚入口函数main,异步编程模式意味着在执行main函数的主线程下同时并行且非阻塞地运行一个或多个任务。

这种异步编程模式带给我们的最主要的好处就是,可使程序的性能和响应速度得到大幅提升。这也是我们一直所追求的目标,无论作为开发者还是消费者,我们都不希望一直处于等待、等待、再等待的状态。所以通过异步编程模式,消费者可以在明知道某个任务会消耗很长时间的情况下,通过无障碍地使用其他功能,仍能获得良好的体验。

并发

可以这么说,并发很好地利用了CPU时间片的特性,也就是操作系统在当前时间片内选择并运行一个任务,接着在下一个时间片内选择并运行另一个任务,并把前一个任务设置成等待状态。其实这里想表达的是,并发并不意味着并行。

具体介绍几种情况,分别如下。

  • 有时候多线程执行会提高应用程序的性能,而有时候反而会降低应用程序的性能。这在JDK中Stream API的使用上体现得很明显。如果任务量很小,而我们又使用了并行流,反而降低了性能。

  • 我们在多线程编程中可能会同时开启或者关闭多个线程,这会产生大量的性能开销,也降低了程序性能。

  • 当我们的线程同时都处于等待I/O的过程中时,并发可能会阻塞CPU资源,其造成的后果不仅是用户等待结果,而且会浪费CPU的计算资源。

  • 如果几个线程共享了一个数据,情况就变得有些复杂了,我们需要考虑数据在各个线程中状态的一致性。为了达到这个目的,我们很可能会使用Synchronized或者lock。

现在,你应该对并发有一定的认知了吧。并发确实是一个好东西,但并不一定会实现并行。并行指的是在多个CPU核上同一时间运行多个任务,或者一个任务分为多块执行(如ForkJoin)。在单核CPU上就不要考虑并行了。

补充一点,实际上,多线程就意味着并发,但是并行只发生在将这些线程于同一时间调度分配到不同CPU核上执行的时候。也就是说,并行是并发的一种特定形式。

并行开发初探

现实中,再大的项目落实到细节上其实都是由多人协作完成的,而每个人在做自己的工作的时候只能一步步地做。可以说项目会被分解成一个个模块,由几个部门一同开发,而在每个部门中其又会被分成一个个子任务,由每个人来负责,最后协作汇总。

我们都知道,CPU也是一个接着一个指令地执行任务的。根据上面描述的场景,我们可以做类比,这里所谓的并行开发就是将一个大任务拆成许多部分来同时执行的。这样就更好地利用了多处理器多核环境,而拆分的每个任务都是独立执行的,并且每个任务彼此之间的执行顺序也没什么关系。通过并行执行,大型问题的解决速度直接快了很多,同时也可以更加高效地利用内存。

基于线程模型,我们可以通过良好的控制来实现用户访问功能的流程,但是为了后台服务器能够更好地进行数据的处理,可以根据需要将并发处理设定为一个开关操作。此操作可自动根据任务的数量来分配线程,同时我们需要一套处理数据的模型。从架构层面,我们拿消息中间件来说,消息中间件连接的各个工程作为一个整体就可以看成数据的一种流向处理,客户端通过订阅生产所设定的Topic来做数据的接收处理。而往小的API方向考虑,可以试着理解JDK中流的概念或者Linux命令中管道的概念(只不过管道和流都是基于推的模式),从中抽象出一个更好用的模型,然后将并发处理的开关操作作为其中的一环加入即可。这时,响应式编程(Reactive Programming)就呼之欲出了。

流(Stream)

因为本书后面会多次使用流的概念,所以在此专门介绍一下。在我们平时的编程开发中,往往练就了一番使用Collections框架提供的API熟练、高效地处理数据的能力,对于较复杂的数据,我们往往会用循环来检查。为此,Collections API针对循环遍历的上手难度和方便性做的优化最多,但此优化只适用于从上到下依次执行,并不能高效地利用我们的多核CPU,而通过流则可以很好地解决这个问题。

其实对于流,完全不需要将其想得多么高大上,其就像一条河中的水,有源头,有处理过程,同样有最好的消费归宿。“滚滚长江东逝水”这句歌词恰好道出了流的一次性特性。

通过上面的简单场景,可以总结流的特性如下。

  • 流中的元素是有顺序的。

  • 流需要一个数据源。流可以将集合、数组、文件或者其他的I/O资源作为其输入的数据源。

  • 丰富而流畅的处理操作API。流API提供了与其他函数式语言相同的API(具体API的名字都一样,基本没有什么学习难度),这方便了统一操作,如filter、map、skip、limit等操作。

  • 包含一些隐式操作。比如,针对元素迭代,我们无须手动指定,这是默认实现的。另外,在最终具体消费的时候,才会执行流的整个处理过程。

响应式流(Reactive Stream)

从消息中间件的使用场景可知,在这个数据爆炸的时代,我们的需求在升级,不仅是前台访问后台,取一组数据;而且更多的是面对海量数据,这时需要将其过滤、修改、转换为我们所需要的数据,而处理这些数据需要大量的时间。为了突破这个瓶颈,我们可以使用流来快速处理和响应。也正是基于此,下面来学习一套类似于消息中间件整体使用场景的API,为此需要了解响应式流API的规范和设计思想,以更好地服务业务,而无须苦苦挣扎,使用最基础的流API。

何为消息中间件,从简单的场景理解它就像QQ。比如,我们都加入了QQ群,而作为群中的一员,我们既是消息的生产者,也是消息的消费者。群中的成员想要获取消息,首先要加入这个群,即订阅,于是生产、消费、订阅这几个动作就都产生了。

下面介绍一下响应式流的特性。

异步

前面已经有所介绍,在传统开发模式下,我们是一个方法接着一个方法执行的,最终消耗的时长是这些方法消耗的总时长。而基于异步编程模式,由于这些操作是同时并行执行的,因此最终消耗的时长是这些操作中消耗时间最长的操作所消耗的时长。也就是说,在大量任务需要执行的情况下,异步编程模式为我们的程序带来了快速响应。

背压(Back Pressure)

背压是响应式流的一个规范。比如,上游滔滔江水来,下游河道窄浅,一旦发生洪水,后果将不堪设想,为了减轻下游的压力,索性就建设大坝。回到响应式流,流中的元素会由生产者(Producer)在一端生产出来,而在另一端由消费者(Consumer)消费掉。一旦元素的生产速度超过了消费者的消费速度,就会造成产品的积压,即元素的积压。随着这种积压的不断增加,程序性能就会下降,直至程序“挂掉”。背压(Back Pressure)就是用来解决这个问题的,虽然它可能会增加元素的处理时间,但是它建立起了一个弹性机制,允许程序内部按需调节而不至于使程序崩溃。

具体来说就是,元素由发布者生产、发布,由订阅者或消费者在下游收集。接下来,消费者会根据需求发送一个信号给上游,以此来保证可以将所需元素安全地推送给消费者,而发送信号这个动作是异步进行的。对于订阅者来讲,其可以通过一个拉取策略来发送更多的请求以获取元素。

响应式开发的设计原则

从异步的角度可以知道,我们往往会将一个任务拆分成许多小任务,各个小任务之间可以互不阻塞地异步执行(分而治之),在每一个任务都完成后,将它们的结果进行组合并生成一个结果流。我们通过响应式编程设计即可很轻松地做到这一点。下面介绍一下响应式开发应该遵循的一些设计原则。

响应式系统提供了诸如可响应能力、高可用性、弹性机制、消息驱动和可扩展性等机制,从而确保响应式编程不会使系统资源一直被占用,这样可以使系统的其他组件正常运行。

可响应能力

可响应能力是一个应用程序最重要的功能,在编程的时候我们希望能有一个高效、统一的格式,包括对错误的处理响应,这一点在Web开发过程中已经很常见了。

高可用性

我们的服务器可能会挂掉,无法提供服务。这时可以提供一个备用服务器,这样在主服务器宕机的时候,备用服务器能够顶替上来,达到服务不间断的效果。于是我们看到日常开发中经常会提到各式各样的集群,集群的一个目的就是实现服务不间断。同样,应用程序可能会被拆分为很多系统模块并做成微服务,微服务之间彼此隔离。如果一个微服务挂掉,并不会影响整个系统提供服务,可将损失最小化。

弹性机制

每当有数据到达时,系统会根据需要分配计算资源,以保证数据得到及时处理。响应式系统提供了一个弹性算法机制,当资源需求增长时,分配的计算资源也随之增长;当资源需求减少时,多分配的资源也会随之回收,避免浪费。

消息驱动

简单点讲,消息驱动就是将每个人比作系统中的一个个微服务组件,人与人之间的交流通过消息进行。从这个场景来看,响应式系统通过使用异步的消息在各个组件间交流、通信,这样就可以成功地做到各个组件之间的隔离和松耦合,也就可以更轻松、灵活地扩展和维护系统了。

可扩展性

开发中经常会面对持续不断出现的新需求。可以这样说,我们现在开发的软件无法完全满足未来的需求,不仅可能会因为需要添加新功能而无法满足,而且可能会因为日益增长的访问压力而逐渐无法满足,并且这个压力可能仅仅集中施加在几个模块上。在理解了这些之后,我们要做的不仅是在一个项目中添加代码,而且还要在不影响旧代码的前提下,对这个项目进行扩展。部分功能模块可以进行水平扩展(集群化),也可以单独开发微服务进行云平台挂载(即功能性的垂直扩展),而路由和相关的其他模块只需要修改极少的代码。

响应式开发的好处

响应式开发的好处主要包含以下几点。

  • 提高所开发程序的性能。

  • 在多核机器上,提高了计算资源的利用率。

  • 为异步编程提供了一个更靠谱的可维护方案。

  • 提供了背压机制,也就是对计算资源提供了过载保护功能。

响应式开发工具库

已经有很多工具库实现了响应式流的标准,包括Akka、Reactor、RxJava、Streams、Vert.x等。下面简单介绍几种,在后面的章节中我会重点讲解RxJava(关于Reactor,会在本系列丛书的另一本书中具体讲解)。

RxJava简介

通过官方GitHub可知,RxJava是使用Java语言开发的专门针对JVM的一种响应式扩展工具,通过它可以轻松地在服务器端实现并发操作。RxJava的目的就是处理客户端越来越复杂的请求,在服务器端通过并行计算快速地响应请求。

接下来,我们开始了解RxJava到底是怎么一回事,作为数据的消费者,我们会对获得的数据做出各种反应。有句话说得好,“跳出三界外,不在五行中”,在我们以旁观者的视角来看这件事的时候,其实它就是一个观察者模式的实际体现。RxJava下的响应式编程其实就是基于影院里的电影(Observable)提供的内容(生产者数据)传播给订阅者,然后订阅者做出相应的反应。

结合上面的场景,下面对RxJava所涉及的要点进行解释。

  • Observable:表示数据源,Observable会发出一定数量的元素,发送可能会成功,也可能在这个过程中出现状况而失败。从电影院的场景可以知道,同一时间Observable可以有多个订阅者。

  • Observer或Subscriber:表示订阅者,通过监听Observable来消费Observable所发送的元素。

  • Methods:表示一系列操作API,对下发数据进行加工整合。

  • onNext:在一个元素被Observable发送出去的时候,通过该方法可以调用每一个订阅者。

  • onComplete:在Observable成功发送完所有数据后,会调用这个方法来收尾。

  • onError:当Observable发送数据的过程中出现错误的状况时,会调用这个方法结束发送并返回一个error事件。

  • RxJava所带来的好处主要如下。

  • 允许我们进行一系列的异步操作。

  • 有时为了跟踪状态,我们会通过一个原子类变量保存之前计算的值。我们无须专门使用原子类来跟踪状态,因为RxJava中已经封装了这些操作。

  • RxJava提供了一个在整个执行过程中发生错误时的处理途径。

Reactor简介

Spring 5官方文档提到,其通过Reactor的支持,在服务器端获得了更高的性能和更快的响应速度。SpringWebFlux作为新一代的Web开发框架,以Reactor作为基础框架进行异步编程的开发,从而可以使我们写出性能更好的Web应用程序。如果大家看过我博客中关于Spring 5源码分析的系列文章,就可以知道Spring MVC框架的整个运行过程其实使用了事件驱动(Event-Driven)模式,而Reactor自身的设计也使用了这个模式。参考前面的电影院场景,当电影的画面和声音传到你的眼睛和耳朵中时,会引发你喜怒哀乐的情绪反应。再形象一点,在拳击选手一个直拳要打到对方选手脸上的时候,对方选手会躲闪。电影的画面和拳击选手的直拳动作都是一种事件的表现,根据事件做出相应处理的整个过程就是所谓的事件驱动。在Spring里,这就相当于我们的后台服务器接收事件请求,通过multicaster多路分发器来分发给相应的监听器(Listener),最后由监听器里定义的相应的handler来做具体处理。整个过程大概就是这样的。

知道了上面的这些内容,通过使用Reactor,我们写出的程序就可以按照事件驱动的模式很轻易地异步运行了。

1.Reactor的优点

Reactor支持完全无阻塞,其主要的目标之一就是解决传统Web开发方案对于异步支持的各种弊病。它提供了十分有效的途径来支持背压。它还有以下优点。

  • 丰富的API,可以对数据流进行操作。

  • 提供了一种可读性更强的代码书写方式,使我们所写的代码可以更方便地得到维护。

  • 与流相同,无消费,不执行。

  • 消费者具备发信号通知生产者元素按需下发的能力(RxJava同样具备)。

2.Reactor的核心功能

Reactor项目的主要模块是reactor-core,这是一个专门用于支持响应式流规范的类库,其支持Java 8及后续版本。通过查看Reactor API,可知它和RxJava很像。Reactor3是Reactor 2和RxJava的核心贡献开发者一起完成的一个混合版本,这也是本章把这几个东西放在一起介绍,然后分章讲解的很重要的原因,因为这样更易于理解。

Reactor与RxJava有相同的Publisher、Subscriber、Subscription和Processor核心接口。这里只简单介绍Publisher最常用的两个实现Mono和Flux,以及相关的操作符。

  • Mono:表示一个特殊的Publisher,它可以发送0个或1个元素。

  • Flux:表示一个特殊的Publisher,它可以发送0到n个元素。

  • 操作符:元素在从Publisher发送给订阅者之前,可能会需要进行一些处理,包括转换、过滤操作等。

MongoDB简介

在后面的实战开发中,我们可能会用到MongoDB,官方提供的MongoDBReactive Streams Java版本的驱动包API可以对MongoDB进行异步流处理,而且是无阻塞支持背压的。这里只是提一下,说明我们的数据库操作层面也开始做到了对响应式流API标准的支持。

响应式项目用例

前面说了那么多,大家可能依然有点不明白,那么为了更好地理解响应式系统(Reactive System),我们看看它与传统项目的不同之处。

以我们生活中的股票场景为例,我们需要看到股票信息的实时动态展示。这时我们会打开并保持一个页面,这个页面可以实时显示股票信息。开发人员需要做的是,将最新的数据更新到这个股票展示页面上。作为股民,面对的是“差之毫厘,失之千里”的局面。对他们来说,数据刷新得越及时,对决策越有利(在这里,我们只从响应式的角度来考虑这个问题,现实项目中会有基于WebSocket的实现,Spring MVC中也有SSE的实现)。

传统开发模式

根据以往的开发经验,我们会主动地检查股票价格有没有变化,如果有变化,就从后台拉取最新的数据。如图1-1所示的流程图就代表着传统开发模式。                            

在传统开发模式下,一旦开始渲染访问页面,就会每隔一段时间(图1-1中是1分钟)发送AJAX请求到后台的查询服务去请求股票价格数据。使用这种方式,无论股票价格是否真的发生变化,都会去请求,但无法保证股票价格的变化会被立刻传递到Web页面上。

响应式开发模式

响应式开发模式通过事件驱动的方式将各个组件连接到一起,以实现在事件发生时其他组件可以立即进行响应。

也就是说,在加载股票价格页面后,这个页面会有一个专属ID注册到股票查询服务上。一旦使股票价格发生变化的事件产生,这个事件(Event)就会触发响应,最新的股票价格就会在Web页面上进行更新显示。如图1-2所示说明了整个流程。

可以看到,响应式开发模式一般包括下面3个步骤。

(1)订阅(Subscribing)事件。

(2)事件的发生与传播。

(3)解除订阅。

在股票价格页面初始化加载的时候,其中有一个动作就是订阅当前使股票价格发生变化的事件源,可以认为事件源是消息中间件里的主题(Topic)(或者是我们订阅的一个RSS主题),而不同的响应式框架会有不同的具体方式,使用消息中间件也可以实现订阅。

在我们所关注的某只股票价格发生变化的时候,一个新的事件就会产生并分发给这个事件的订阅者。我们的Web页面会及时地接收并更新股票价格数据。而一旦Web页面关闭或者刷新,一个解除订阅的请求就会被发送至后台。

传统开发模式和响应式开发模式的比较

可以看到,传统开发模式是比较简单的,而响应式开发模式需要我们实现一个订阅和事件传播链。如果事件的传播需要跨项目,也就是涉及其他项目,那么就可能会使用到消息中间件,这将会变得复杂,其并不属于本书的范围,此处不做讨论。

在传统开发模式中,更新股票价格页面主要是基于盲目的主动拉取来实现的,前端根本就不知道会不会有数据发生变化。这也就意味着无论后台数据有没有发生变化,前端都需要定时从后台拉取一次数据。而在响应式开发模式中,一旦注册订阅了价格变动事件,那么只有这只股票的价格发生变化才会触发一系列的操作,这样明显提高了程序性能和用户体验。

在传统开发模式中,这个例子中线程的生命周期会比较长,这也就意味着该线程所使用的资源在这个过程中会被线程锁定。考虑到同一时刻服务器会接收大量的请求,这样势必会造成更多的线程相互争夺资源。在响应式开发模式中,线程生存的时间短,这也就意味着争夺资源的情况较少。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值