当前位置: 首页 > 科技观察

什么是响应式编程?

时间:2023-03-12 13:47:04 科技观察

近年来,随着Go、Node等新语言和新技术的出现,Java作为服务端开发语言的老大地位受到了挑战。虽然Java的市场地位短时间内不会改变,但Java社区依然视挑战为机遇,努力不断提升应对高并发服务端开发场景的能力。为了应对服务器端高并发的开发场景,2009年,微软提出了一种更加优雅的异步编程实现方式——ReactiveProgramming,我们称之为响应式编程。随后,各门语言也迅速跟进,各有各的响应式编程实现。例如JavaScript语言通过ES6中的Promise机制引入了类似的异步编程方式。与此同时,Java社区也在迅速发展。Netflix和LightBend提供了RxJava和AkkaStream等技术,让Java平台也有了实现响应式编程的框架。目前我们其实可以通过Mina、Netty等NIO框架完成高并发下的服务端开发任务,但是这样的技术只掌握在少数资深开发者手中,因为难度大,不适合大多数普通开发者.虽然很多公司已经在实践响应式编程,但总体来说,它的应用范围还是有限的。造成这种情况的原因是缺乏简单易用的技术。这些技术需要让响应式编程更加流行,并像SpringMVC一样将各种技术与Spring提供的服务集成起来。2017年9月28日,Spring5正式发布。Spring5的发布最大的意义在于它在响应式编程技术的普及上迈出了一大步。与此同时,后台支持Spring5响应式编程的框架SpringReactor也进入了3.1.0里程碑版本。什么是反应式编程?在现实生活中,当我们听到有人叫我们的名字时,我们会做出反应,也就是说,我们是基于事件驱动模型来编程的。所以这个过程其实就是发送生成的事件,然后我们作为消费者,对发送的事件进行一系列的消费。从这个角度来说,整个代码的设计应该是面向消费者的。比如看电影,有些画面我们不想看,就闭上眼睛;如果我们不想听到某些声音,我们应该捂住耳朵。其实这就是给消费者的强化包装。我们把复杂的逻辑拆分,然后分成小的任务进行包装,所以就有了filter、map、skip、limit等操作。01并发与并行的关系可以说,并发很好地利用了CPU时间片的特性,即操作系统选择并运行一个任务,然后在下一个时间片运行另一个任务,并设置上一个任务等待状态。事实上,并发并不意味着并行。具体列举以下几种情况。①有时,多线程执行会提高应用程序的性能,但有时也会降低应用程序的性能。这在JDK中使用StreamAPI时很明显。如果任务负载很小并且我们使用并行流,它会降低应用程序的性能。②在多线程编程中,可能会同时开启或关闭多个线程,这会产生大量的性能开销,降低应用程序的性能。③当线程同时在等待I/O的过程中,并发可能会阻塞CPU资源。其后果不仅是用户等待时间过长,而且浪费了CPU计算资源。④如果几个线程共享一份数据,情况会变得有点复杂。我们需要考虑每个线程中数据的状态是否一致。为了达到数据一致性的目的,很可能会用到synchronized或者lock相关的操作。现在,你对并发有了一定的了解。并发很好,但不一定会导致并行。并行是在多核CPU上同时运行多个任务或者将一个任务分成多个块同时执行(如ForkJoin)。如果你有一个单核CPU,就不要考虑并行性。补充一下,多线程其实就是并发,但是只有当这些线程被同时调度,分配给不同的CPU执行时才会出现并行。也就是说,并行是并发的一种特定形式。一个任务中往往会产生很多元素,而这些元素中的大部分在不参与运行时只能在当前线程中。这时候我们就可以对它们进行ForkJoin,但是这对于很多程序员来说有时候是很难操作的。控制有点难上手。这时,如果你使用响应式编程,你可以通过提供的调度API轻松发送和分发事件元素。它会将每个元素打包成一个任务,提交给线程池。任务是计算类型还是I/O类型选择对应的线程池。这里需要强调的是,线程只是一个对象。不要认为它是CPU中的某个执行核心。这是很多人都会犯的错误。CPU时间片会切换执行这些线程。02如何理解响应式编程中的背压和背压,翻译自BackPressure,从英文字面意思看,叫背压可能更合适。首先解释背压。这就像用吸管喝饮料。吸管内的气体被吸出,吸管内形成低压,进而产生从饮料到吸管的吸力。这种吸力将饮料吸入口中。我们常说人往高处流,水往低处流。之所以在水中会出现这种现象,其实是重力造成的。但是现在吸管下面的水上升到人的嘴里,说明从下游到上游有一个反向压力,这个反向压力大于重力,可以称为背压。这是一个很直观的词,向后向后压——BackPressure。在程序中,也就是在数据流从上游源producer传输到下游consumer的过程中,如果上游source的生产速度大于下游consumer的消费速度,那么下游可以想象为一个容器,它无法处理数据,然后数据就会从容器中溢出,就会出现类似稻草例子的情况。现在,我们要做的就是为这种场景提供一种解决方案,我们称之为背压机制。为了更好的解决背压带来的问题,让我们回到现实来看一个东西——大坝。洪水期间,下游不可能一下子耗掉那么多水。大坝此时的作用是截洪,根据下游消耗情况酌情泄洪。也就是说,背压机制应该放在连接元素生产者和消费者生产者的地方,也就是生产者和消费者之间的纽带。那么根据上面大坝的描述,背压机制应该具备承载元素的能力,也就是必须是容器,存储和传递的元素应该是有序的,所以这里使用队列是最合适了。背压机构仅仅起到承重作用是不够的。因为上游有压力,下游可以按需索取元素,也可以根据实际情况在中间限流。这样上下游共同实现背压机制。本书后续内容及相关配套视频中会介绍与背压相关的API。03Reactor和RxJava的比较关于响应式编程,我写的《Java 编程方法论:响应式RxJava 与代码设计实战》的书已经出版了,那么Reactor和RxJava有什么区别呢?首先我想明确的告诉你,如果你使用的是Java8+,那么推荐使用Reactor3,如果你还在使用Java6+或者功能需要异常检查,那么推荐使用RxJava2。从上图可以看出,RxJava2和Reactor共享一套接口API标准ReactiveStreamsCommons,这也说明了它们的最终目的是一致的,API是通用的,也降低了学习成本。让我们再回顾一下RxJava。到目前为止,RxJava的发布主要分为三个大版本RxJava3、RxJava2和RxJava1。与RxJava1不同的是,RxJava3和RxJava2直接通过新增的Flowable类型实现了Publisher的接口定义(没有太大区别介于RxJava3和RxJava2之间,所以这里只介绍RxJava2)。同时,RxJava2仍然保留了RxJava1中的Observable、Completable和Single,并引入了Single的升级版,支持Optional——Maybe类型。RxJava1中的Observable不支持RxJava2中的背压机制,背压机制是Flowable的专有功能,但Observable内部提供了可转换的API。需要注意的是Observable在RxJava2中实现了自定义的ObservableSource接口,在Reactor中可以发现Mono和Flux都实现了Publisher接口,并且都实现了背压机制。Flux可以类比为RxJava2中的Flowable类型,Mono可以理解为RxJava2中Single的增强版,后面会做更深入的讲解。同样,我们来看看Reactor和RxJava的区别。RxJava为了兼容Java1.6+,不得不自己定义一些函数式接口,可以参考io.reactivex.functions下的接口定义。Reactor3是基于JDK中提供的java.util.function设计实现的。从java.util.stream.Stream到Flux,从后者到前者的转换很容易。同样,您可以轻松地在CompletableFuture和Mono之间进行转换,您可以轻松安全地创建基于Optional类型元素的Mono。Reactor3可以更好地服务于SpringFramework5,并且更兼容最新版本的JDK。最后简单介绍一下上图中的几个部分。Core是我们主要研究的库,是Reactor的核心实现库。其作用与RxJava2的核心实现相同,本书主要介绍reactor-core模块。IPC可以被认为是一个背压支持组件,设计用于编码、解码、发送(单播、多播或请求/响应)和服务连接。IPC支持Kafka、Netty和Aeron。插件包括reactor-adapter、reactor-logback和reactor-extra。reactor-adapter可以说是连接RxJava1/2中的Observable、Completable、Flowable、Single、Maybe、Scheduler的桥梁,可以很方便地与Reactor3进行转换。同样,这个库也针对Swing/SWT做了针对性的适配调度器和Akka调度器。reactor-logback用于支持ReactorCore对Logback函数的异步处理。reactor-extra为数字通量源提供了许多数学运算。ReactiveStreamsCommons是RxJava2和Reactor共享的一套接口API标准。