谈谈我对Reactive Programming的理解

720 阅读5分钟

Microsoft于2012年的时候在.NET生态中实现了反应式扩展库,简称ReactiveX或Rx。跟着RxJava又开发了JVM上的实现。之后Pivotal、Netflix、LightBend和Twitter等厂商联合建立了Reactive Streams,并在2015-04-28发布1.0版本。并由Doug Lea通过JEP-266 More Concurrency Updates提案增加了Flow API包括了在JDK9中。

或许以前你没怎么听说过Reactive Programming,但随着Spring5的发布,WebFlux、R2DBC、RSocket等各种名词层出不穷,Reactive相关的文章不断,基于国内主要使用Spring全家桶居多,一下子Reactive Programming成为热门技术,似乎是Spring将Reactive Programming带到了新高度,但其实主流的微服务框架Helidon、Micronaut、Quarkus和Vert.x等都支持Reactive Programming。

什么是Reactive Programming?

这是在最开始学习Reactive Programming(后文统称为反应式编程)的时候问得最多的问题了,根据维基百科的定义:

反应式编程是一种面向数据流和变化传播的声明式编程范式。

en.wikipedia.org/wiki/Reacti…

看完介绍可能还是难以理解具体什么是反应式编程,因为它没有更详细的定义描述,没有结合具体编程语言进行讲解,反应式编程解决了什么问题?我们为什么要使用反应式编程?带着这些问题,从我自己的角度谈谈对反应式编程的理解。

首先反应式编程主要面向的是数据编程,而非面向逻辑编程。数据流在Java中对应Stream,涉及到流就会有过滤、转换、聚合和拆分,还有缓冲、调整流速率操作等等。变化传播类比观察者模式(Observer Pattern),当数据发生改变时,才会对其进行响应。另外变化传播也意味着异步非阻塞,Java使用Future表示异步任务结果,在获取执行结果时可能导致阻塞。而CompletableFuture能够真正的进行异步非阻塞操作,一旦任务完成触发回调执行下游方法,并可链接多个操作。

那么我的理解就是在Java中反应式编程(Reactive) = 数据流(Stream) + 异步(CompletableFuture) + 背压(BackPressure)。我们没办法使用Java的Stream和CompletableFuture很好进行数据流的异步编程。首先Stream使用Iterator进行数据读取,虽然提供了parallel并行操作,但是其终止操作依旧是阻塞的。Stream的数据是静态的,无法动态生成,另外缺少部分高阶操作如缓冲、窗口等。

Stream.of(1, 2, 3)
        .parallel()
        .map(String::valueOf) // paralleled
        .collect(Collectors.toList()); // blocked

CompletableFuture支持异步链式回调,并可指定Executor对应执行线程池,虽然提供了allOf方法,但无法对多个(流)异步操作进行组合处理。anyOf为当其中一个操作完成时结束,不能中断其它未完成的操作。

CompletableFuture
        .allOf(f1, f2, ...)
        .thenApply(tt -> {
            // where my result?
            // need result? 
            Object result1 = f1.join();
            Object result2 = f2.join();
        });
CompletableFuture
        .anyOf(f1, f2, ...)
        .thenApply(tt -> {
            if (!f2.isDone()) {
                f2.cancel(true); // CompletableFuture cancel not working.
            }
        });

BackPressure指上游生产者的生产速率大于下游消费者的消费速率时的流控处理。一种是消费者根据自身能力使用Pull的方式进行数据获取;另一种则是生产者调整流控,当消费者来不及消费时,将数据进行缓冲、丢弃等。

那么反应式编程解决了什么问题呢?我们为什么要使用反应式编程?我们以Tomcat为例,Tomcat默认采用一个请求一个线程的方式,假设配置的线程池大小为100,响应时间为100ms/r,那么系统的QPS能够达到1000r/s。假定在固定线程池大小不变的情况下,因为更多的线程意味着更多的上下文切换和内存消耗,如果因为一些阻塞操作如IO等导致线程阻塞,加大了响应时间,那么整体系统的QPS将降低。另外通常我们的业务系统的基本流程为请求(解析Json) -> 数据交换(Database) -> 响应(组装Json),而我们可以使用反应式编程,把请求当作数据流,基于数据流,根据反应式编程框架提供的操作符,声明式的组装业务流程和逻辑,并且包含高阶的并发抽象,异步编程会更容易,更充分地利用系统资源。我们此处暂不论Servlet异步API,因为它同样面临上面提到的异步编程困境。

反应式编程不是银弹,它的缺点?

  • 可维护性
    代码的关注重点由以前的逻辑代码转为围绕反应式编程框架提供的操作符。
  • 排查异常困难
    当代码出现异常时,异常堆栈可能全是操作符的嵌套调用,无法得知真正的异常位置,虽然部分反应式框架提供了调试工具,依旧加大了异常排查难度。
  • 编程难度
    反应式编程异步非阻塞的特性,使得原来那些如同步监控指标统计或依赖线程上下文的操作都将不能工作,另外反应式编程通常和函数式编程结合,反应式框架本身就有一定的学习成本,降低开发效率。

Reactive Programming和Reactive System的区别?

  • Reactive Programming

Reactive Programming是一种面向数据流的异步编程范式,通常应用在单个组件或者服务上。

  • Reactive System

Reactive System 则是系统级别的,比如分布式系统。反应式宣言描述了符合反应式系统的设计原则,其中包括Responsive、Resilient、Elastic和Message Driven几个特性。

总结

Reactive Programming是非常有意思的,并且值得所有开发人员学习,但是你可以在工程上完全不使用Reactive Programming,这取决于你的系统能够从中获取什么,并承受相应带来的弊端。但是例如在一些负载均衡代理方面的应用,Reactive Programming有极大的优势,或者涉及到一些数据流编排,如多个微服务API链式调用,Reactive Programming能够很轻松的表达异步调用。

Reference