最近几年,随着Go、Node 等新语言、新技术的出现,Java 作为服务器端开发语言老大的地位受到了不小的挑战。虽然Java 的市场地位在短时间内并不会发生改变,但Java 社区还是将挑战视为机遇,并努力、不断地提高自身应对高并发服务器端开发场景的能力。

      为了应对高并发服务器端开发场景,在2009 年,微软提出了一个更优雅地实现异步编程的方式——Reactive Programming,我们称之为响应式编程。随后,各语言很快跟进,都拥有了属于自己的响应式编程实现。比如,JavaScript 语言就在ES6 中通过Promise 机制引入了类似的异步编程方式。同时,Java 社区也在快速发展,Netflix 和LightBend 公司提供了RxJava 和Akka Stream 等技术,使得Java 平台也有了能够实现响应式编程的框架。

   在2017 年9 月28 日,Spring 5 正式发布。Spring 5 发布最大的意义在于,它将响应式编程技术的普及向前推进了一大步。而同时,作为在背后支持Spring 5 响应式编程的框架Spring Reactor,也进入了里程碑式的3.1.0 版本。

   响应式编程是一种面向数据流和变化传播的编程范式。 这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

Reactive 响应式编程 01 简介_java


Reactive 响应式编程 01 简介_spring_02

Reactive 响应式编程 01 简介_spring_03

Reactive 响应式编程 01 简介_java_04

比较以下的类

CompletableFuture

Stream

Optional

Observable (RxJava 1)

Observable (RxJava 2)

Flowable (RxJava 2)

Flux (Reactor Core)


一、Composable  可组装 

Java : 构建流水线,传递单一值,异常对象

RxJava 和 Reactor:链式编程,可以传递N个值

CompletableFuture - 提供很多的.then*()方法,这些方法允许我们构建一个流水线,在不同的执行阶段之间传递一个单一的值(或者没有值),以及传递异常对象。

Stream - 提供很多的可以链式编程方式连接起来的操作,不同的操作阶段之间可以传递N个值。

Optional - 提供一些中间操作,如: .map(), .flatMap(), .filter().

Observable, Flowable, Flux - 跟Stream相同

二、Lazy 延迟执行

Java : 不支持延迟执行

RxJava 和 Reactor:延迟执行,有订阅者的时候才会执行

CompletableFuture - 非延迟执行,它本质上只是一个异步结果的持有者。这些对象创建出来是为了代表对应的工作,CompletableFuture创建的时候,对应的工作已经开始执行了。它不知道关于工作的任何内容,只是关心结果。所以,没有办法能走到上游去从上到下执行整个流水线。当结果被塞到CompletableFuture对象的时候,下一个阶段开始执行。

Stream - 所有的中间操作都是延迟执行的。所有的终端操作,会触发整个计算。

Optional - 非延迟执行,所有的操作会马上发生。

Observable, Flowable, Flux - 延迟执行,没有订阅者的话,什么都不会做,只有当有订阅者的时候才会执行。

三、Reusable 可重用

Java : 支持重用

RxJava 和 Reactor:支持重用

CompletableFuture - 可以重用,它只是在一个值外面做了一层包装。但需要注意一点,这个包装是可更改的。.obtrude*()方法会更改它的内容,如果你确定没有人会调用到这类方法,那么重用它还是安全的。

Stream - 不能重用

Optional  - 完全可重用,因为它是不可变对象,而且所有工作都是立即执行的。

Observable, Flowable, Flux - 就是设计来可重用的。所有的执行会从初始点开始,走过所有阶段,前提是有订阅者。

四、Asynchronous(异步)

Java : 支持异步

RxJava 和 Reactor:支持异步

CompletableFuture - 嗯...这个类存在的目的就是异步的把多个操作链接起来。CompletableFuture代表一个工作,后面跟一个Executor关联起来。如果你不明确指定一个executor,那么系统会使用公共的ForkJoinPool线程池来执行。这个线程池可以用ForkJoinPool.commonPool()获取到。默认的设置下它会创建系统硬件支持的线程数一样多的线程(通常就是跟CPU的核心数,如果你的CPU支持超线程,那么可能再翻一倍)。不过你也可以设置ForkJoinPool线程池的线程数,用以下JVM option:

或者每次调用的时候提供一个定制的Executor。

Stream - 不支持创建异步过程,但是可以支持并行的计算——通过stream.parallel()等方式创建并行流。

Optional - 不支持,它只是一个容器。

Observable, Flowable, Flux - 目标就是为了构建异步的系统,但是默认情况下还是同步的。subscribeOn和observeOn允许你来控制消息的订阅以及消息的接收(指定当你的observer的 onNext / onError / onCompleted 被调用的时候做什么事情)。

subscribeOn让你决定用哪个Scheduler来执行Observable.create

五、Cacheable(可缓存)

Java :可缓存

RxJava 和 Reactor:默认不可以缓存,但是调用.cache()后可以

可缓存和可重用之间的区别是什么?举个例子,我们有一个流水线A,并且使用这个流水线两次,创建两个新的流水线 B = A + 以及 C = A + 。

      - 如果B和C都能成功完成,那么这个A是可重用的。

      - 如果B和C都能成功完成,并且A的每一个阶段只被调用了一次,那么这个A是可缓存的。

可以看出,一个类如果是可缓存的,必然得是可重用的。

CompletableFuture - 跟可重用的答案一样。

Stream - 不能缓存中间操作的结果,除非调用了终端操作。

Optional - ‘可缓存’,实际上,所有工作立即执行,并且做完后就保存了一个不变值,自然‘可缓存’。

Observable, Flowable, Flux - 默认情况下是不可缓存的,但是你可以把一个这些类转变成缓存,只要调用.cache()就可以

六、Push or Pull(推模式还是拉模式)

Java :拉模式 和 推模式

RxJava 和 Reactor:推模式

 Stream 和 Optional - 是拉模式的。你调用不同的方法(.get(), .collect() 等)从流水线拉取结果。拉模式经常与阻塞、同步是相关联的,而这也合理。你调用一个方法,然后线程等待数据。线程会阻塞直到数据到达。

CompletableFuture, Observable, Flowable, Flux - 是推模式的。你订阅一个流水线,然后当有东西可以处理的时候你会得到通知。推模式通常意味着非阻塞、异步。当流水线在某个线程上执行的时候,你可以做任何事情。你已经定义了一段待执行的代码,作为下一个阶段的任务,当通知到达的时候,这个代码就会被执行。

七、Backpressure(反压)

Reactive 响应式编程 01 简介_java_05

Java :不支持背压

RxJava 和 Reactor:支持背压

要做到支持反压,流水线必须是推模式的。

   Backpressure(反压) 描述的是在流水线中会发生的一种场景:某些异步的阶段处理速度跟不上,需要告诉上游生产者放慢速度。直接失败是不可接受的,因为会丢失太多数据。

Reactive 响应式编程 01 简介_spring_06


Stream & Optional - 不支持反压,因为他们是拉模式。

CompletableFuture - 不需要面对这个问题,因为它只产生0个或者1个结果。

Observable(RxJava 1), Flowable, Flux - 提供一组方案解决这个问题。常用的策略是:

     - Buffering(缓冲) - 把所有的onNext的值保存到缓冲区,直到下游消费它们。

     - Drop Recent - 如果下游处理跟不上的话,丢弃最近的onNext值。

     - Use Latest - 如果下游处理跟不上的话,只提供最近的onNext值,之前的值会被覆盖。

     - None - onNext事件直接被触发,不带任何缓冲或丢弃处理。

     - Exception - 如果下游处理跟不上的话,触发一个异常。

Observable(RxJava 2) - 不解决这个问题。很多RxJava 1的使用者用Observable来处理不适用反压的事件,或者是使用Observable的时候不用任何策略处理反压,这会导致不可预知的异常。所以,RxJava 2明确地区分两种情况,提供支持反压的Flowable和不支持反压的Observable。

八、 Operator fusion(操作融合)

Java :不支持

RxJava1 : 不支持

RxJava2 : 支持

Reactor : 支持

Reactive 响应式编程 01 简介_java_07

Reactive 响应式编程 01 简介_spring_08

Reactive 响应式编程 01 简介_java_09

Reactive 响应式编程 01 简介_java_10


上面的内容可以总结为一个表:

Reactive 响应式编程 01 简介_java_11


Reactive 响应式编程 01 简介_java_12

继续看一个例子,为什么使用Reactor

    例如我们现在要用非阻塞的方式调用一个远程服务,当远程接口数据可用时去做一些业务处理。这时候代码怎么写呢?我们需要提供一个回调函数,然后在响应就绪的时候,去调用我们的回调函数。

传统java 异步回调例子

// 以下案例来自 Reactor 官网
userService.getFavorites(userId, new Callback<List<String>>() {
public void onSuccess(List<String> list) {
if (list.isEmpty()) {
suggestionService.getSuggestions(new Callback<List<Favorite>>() {
public void onSuccess(List<Favorite> list) {
UiUtils.submitOnUiThread(() -> {
list.stream()
.limit(5)
.forEach(uiList::show);
});
}

public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
});
} else {
list.stream()
.limit(5)
.forEach(favId -> favoriteService.getDetails(favId,
new Callback<Favorite>() {
public void onSuccess(Favorite details) {
UiUtils.submitOnUiThread(() -> uiList.show(details));
}

public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
}
));
}
}

public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
});

换成reactor写法后,代码简单多了

// 以下案例来自 Reactor 官网
userService.getFavorites(userId)
.flatMap(favoriteService::getDetails)
.switchIfEmpty(suggestionService.getSuggestions())
.take(5)
.publishOn(UiUtils.uiThreadScheduler())
.subscribe(uiList::show, UiUtils::errorPopup);

再看一个例子

CompletableFuture组合操作的例子

我们首先获取ID的列表,然后我们要通过ID获取一个名称(name)和一个统计信息(Stat),并将它们成对组合,所有这些都是异步的

传统java completableFuture代码

CompletableFuture<List<String>> ids = ifhIds(); 

CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> {
Stream<CompletableFuture<String>> zip =
l.stream().map(i -> {
CompletableFuture<String> nameTask = ifhName(i);
CompletableFuture<Integer> statTask = ifhStat(i);

return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat);
});
List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList());
CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]);

CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray);
return allDone.thenApply(v -> combinationList.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
});

List<String> results = result.join();
assertThat(results).contains(
"Name NameJoe has stats 103",
"Name NameBart has stats 104",
"Name NameHenry has stats 105",
"Name NameNicole has stats 106",
"Name NameABSLAJNFOAJNFOANFANSF has stats 121");

使用Reactor改写

Flux<String> ids = ifhrIds(); 
Flux<String> combinations =
ids.flatMap(id -> {
Mono<String> nameTask = ifhrName(id);
Mono<Integer> statTask = ifhrStat(id);
return nameTask.zipWith(statTask,
(name, stat) -> "Name " + name + " has stats " + stat);
});

Mono<List<String>> result = combinations.collectList();

List<String> results = result.block();
assertThat(results).containsExactly(
"Name NameJoe has stats 103",
"Name NameBart has stats 104",
"Name NameHenry has stats 105",
"Name NameNicole has stats 106",
"Name NameABSLAJNFOAJNFOANFANSF has stats 121"
);

可以看出Reactor编程可以通过异步化更好的利用当前CPU的资源,更强大的事件编排能力。


1. 为什么是Reactor模式

像netty这样的精品中的极品,肯定也是需要先从设计模式入手的。netty的整体架构,基于了一个著名的模式——Reactor模式。Reactor模式,是高性能网络编程的必知必会模式。

​tomcat服务器的早期版本确实是这样实现的。

多线程并发模式,一个连接一个线程的优点是:

一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线程只能对应一个socket”的原因。另外有个问题,如果一个线程中对应多个socket连接不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个socket被阻塞了,后面的是无法被执行到的。


缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数太高,系统无法承受,而且,线程的反复创建-销毁也需要代价。

改进方法是:

采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行数据处理。使用Reactor模式,对线程的数量进行控制,一个线程处理大量的事件。

Reactive 响应式编程 01 简介_spring_13

顺便说一下,可以将上图的accepter,看做是一种特殊的handler。

4.3. 单线程模式的缺点:

1、 当其中某个 handler 阻塞时, 会导致其他所有的 client 的 handler 都得不到执行, 并且更严重的是, handler 的阻塞也会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了)。 因为有这么多的缺陷, 因此单线程Reactor 模型用的比较少。这种单线程模型不能充分利用多核资源,所以实际使用的不多。

2、因此,单线程模型仅仅适用于handler 中业务处理组件能快速完成的场景。


Reactive 响应式编程 01 简介_响应式编程_14

Reactive 响应式编程 01 简介_响应式编程_15

7. Reactor编程的优点和缺点

6.1. 优点

1)响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;

2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;

3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;

4)可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;

6.2. 缺点

1)相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。

2)Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。

3) Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用改进版的Reactor模式如Proactor模式。

reactor 线程模型

Reactive 响应式编程 01 简介_java_16

Reactive 响应式编程 01 简介_java_17

Reactive 响应式编程 01 简介_响应式编程_18

Reactive 响应式编程 01 简介_响应式编程_19

Reactive 响应式编程 01 简介_java_20

Reactive 响应式编程 01 简介_java_21

主从Reactor-多线程

主从Reactor-​​多线程​​相当于将原来的Reactor主线程又分成了两部分,系统的复杂度进一步提升。

Reactive 响应式编程 01 简介_响应式编程_22

Reactive 响应式编程 01 简介_spring_23

Reactor模式具有如下优点

响应快,不必为单个同步事件所阻塞,虽然Reactor本身仍是同步的(因为Reactor下面有多个subReactor,所以当一个subReactor阻塞还可以调用其他的subReactor);

可以最大地避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;

扩展性好,可以方便地通过增加Reactor实例个数来充分利用CPU资源;

复用性好,Reactor模型本身与具体时间处理逻辑无关,具有很高的复用性。