响应式编程简介

对于响应式编程的解释

  • 个人理解:
    Java中传统的线程接口有两种,一种是Runable,一种是Callable;一个没有返回值,一个有返回值;Callable的返回值用Future来接收。无论是Runable还是Callable,都要等线程执行完才能继续操作下去,这个过程中就造成了阻塞。
    如果想我把任务分配下去,并且告诉他任务执行完成就怎么怎么样,然后我就不用管了,这样一种思想可以理解成是响应式编程,就像是“好莱坞原则”(演员不用主动联系导演,导演会去联系演员,也就是你不要来找我,我会去找你)。

  • 响应式编程就是当你做一个带有一定延迟的才能够返回的io操作时,不会阻塞,而是立刻返回一个流,并且订阅这个流,当这个流上产生了返回数据,可以立刻得到通知并调用回调函数处理数据。

  • 举个例子:你准备做菜,都准备好了,但是发现没有盐,需要去买,这个买的过程就比较耗时,传统的多线程模式是这样的,你让孩子去超市买,孩子买回来你做饭,但是买的这个过程你是等待着的;而响应式编程的思想是你让孩子去买盐,并且写了一个菜谱给他,他买盐回来就可以按照菜谱做了,做菜这个事情就跟你没有关系了,这样你可以去做其他事情了。

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

Spring的响应式编程:reactor-core

reactor设计模式是基于事件驱动的一种实现方式,处理多个客户端并发的向服务端请求服务的场景。每种服务在服务端可能由多个方法组成。reactor会解耦并发请求的服务并分发给对应的事件处理器来处理。目前,许多流行的开源框架都用到了reactor模式,如:netty、node.js等,包括java的nio。

  • 首先说一下响应流的特点
    • 响应流必须是无阻塞的。
    • 响应流必须是一个数据流。
    • 它必须可以异步执行。
    • 并且它也应该能够处理背压。
      背压是反应流中的一个重要概念,可以理解为,生产者可以感受到消费者反馈的消费压力,并根据压力进行动态调整生产速率。

Publisher

  • 由于响应流的特点,我们不能再返回一个简单的POJO对象来表示结果了。必须返回一个类似Java中的Future的概念,在有结果可用时通知消费者进行消费响应。

  • Reactive Stream规范中这种被定义为Publisher ,Publisher是一个可以提供0-N个序列元素的提供者,并根据其订阅者Subscriber的需求推送元素。一个Publisher可以支持多个订阅者,并可以根据订阅者的逻辑进行推送序列元素。

  • Flux和Mono都是Publisher在Reactor 3实现。Publisher提供了subscribe方法,允许消费者在有结果可用时进行消费。如果没有消费者Publisher不会做任何事情,他根据消费情况进行响应。 Publisher可能返回零或者多个,甚至可能是无限的,为了更加清晰表示期待的结果就引入了两个实现模型Mono和Flux。

Mono

  • Mono 是一个发出(emit)0-1个元素的Publisher,可以被onComplete信号或者onError信号所终止。
  • Mono创建方法:
    • empty():创建一个不包含任何元素,只发布结束消息的序列。
    • just():可以指定序列中包含的全部元素。创建出来的 Mono序列在发布这些元素之后会自动结束。
    • justOrEmpty():从一个 Optional 对象或可能为 null 的对象中创建 Mono。只有 Optional 对象中包含值或对象不为 null 时,Mono 序列才产生对应的元素。
    • error(Throwable error):创建一个只包含错误消息的序列。
    • never():创建一个不包含任何消息通知的序列。
    • fromCallable()、fromCompletionStage()、fromFuture()、fromRunnable()和 fromSupplier():分别从 Callable、CompletionStage、CompletableFuture、Runnable 和 Supplier 中创建 Mono。
    • delay(Duration duration)和 delayMillis(long duration):创建一个 Mono 序列,在指定的延迟时间之后,产生数字 0 作为唯一值。
    • 通过 create()方法来使用 MonoSink 来创建 Mono。

Flux

  • Flux 是一个发出(emit)0-N个元素组成的异步序列的Publisher,可以被onComplete信号或者onError信号所终止。在响应流规范中存在三种给下游消费者调用的方法 onNext, onComplete, 和onError。
  • 创建Flux,通常来讲有4种方法。
    • 使用generate:最简单的同步创建的generate。
    • 使用create:Flux也提供了一个create方法来创建Flux,create可以是同步也可以是异步的,并且支持多线程操作。
    • 使用push:push和create一样,也支持异步操作,但是同时只能有一个线程来调用next, complete 或者 error方法,所以它是单线程的。
    • 使用Handle:Handle和上面的三个方法不同,它是一个实例方法。它和generate很类似,也是消费SynchronousSink对象;不同的是它的参数是一个BiConsumer,是没有返回值的。

创建 Mono 和 Flux(开始阶段)

使用 Reactor 编程的开始必然是先创建出 Mono 或 Flux。有些时候不需要我们自己创建,而是实现例如 WebFlux 中的 WebClient 或 Spring Data Reactive 得到一个 Mono 或 Flux。

  • 使用 WebFlux WebClient 调用 HTTP 接口

    WebClient webClient = WebClient.create("http://localhost:8080");
    public Mono<User> findById(Long userId) {
        return webClient
                .get()
                .uri("/users/" + userId)
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .flatMap(cr -> cr.bodyToMono(User.class));
    }
    
  • 使用 ReactiveMongoRepository 查询 User

    public interface UserRepository extends ReactiveMongoRepository<User, Long> {
        Mono<User> findByUsername(String username);
    }
    

但有些时候,我们也需要主动地创建一个 Mono 或 Flux。

  • 普通的创建方式
    Mono<String> helloWorld = Mono.just("Hello World");
    Flux<String> fewWords = Flux.just("Hello", "World");
    Flux<String> manyWords = Flux.fromIterable(words);
    

这样的创建方式在什么时候用呢?一般是用在经过一系列 非IO型 操作之后,得到了一个对象。接下来要基于这个对象运用 Reactor 进行 高性能 的 IO 操作时,可以用这种方式将之前得到的对象转换为 Mono 或 Flux。

  • 文艺的创建方式
    上面是通过一个 同步调用 得到的结果创建出 Mono 或 Flux,但有时需要从一个 非 Reactive 的 异步调用 的结果创建出 Mono 或 Flux。

如果这个 异步方法 返回一个 CompletableFuture,那可以基于这个 CompletableFuture 创建一个 Mono:

Mono.fromFuture(completableFuture);
如果这个 异步调用 不会返回 CompletableFuture,是有自己的 回调方法,那怎么创建 Mono 呢?可以使用 static Mono create(Consumer callback) 方法:

Mono.create(sink -> {
    ListenableFuture<ResponseEntity<String>> entity = asyncRestTemplate.getForEntity(url, String.class);
    entity.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
        @Override
        public void onSuccess(ResponseEntity<String> result) {
            sink.success(result.getBody());
        }

        @Override
        public void onFailure(Throwable ex) {
            sink.error(ex);
        }
    });
});

在使用 WebFlux 之后,AsyncRestTemplate 已经不推荐使用,这里只是做演示。

处理 Mono 和 Flux(中间阶段)

中间阶段的 Mono 和 Flux 的方法主要有 filter、map、flatMap、then、zip、reduce 等。这些方法使用方法和 Stream 中的方法类似。

下面举几个 Reactor 开发实际项目的问题,帮大家理解这些方法的使用场景:

  • 问题一: map、flatMap 和 then 在什么时候使用
    本段内容将涉及到如下类和方法:

方法:Mono.map()
方法:Mono.flatMap()
方法:Mono.then()
类:Function
在 Mono 和 Flux 中间环节的处理过程中,有三个有些类似的方法:map()、flatMap() 和 then()。这三个方法的使用频率很高。

传统的命令式编程

Object result1 = doStep1(params);
Object result2 = doStep2(result1);
Object result3 = doStep3(result2);

对应的反应式编程

Mono.just(params)
    .flatMap(v -> doStep1(v))
    .flatMap(v -> doStep2(v))
    .flatMap(v -> doStep3(v));

从上面两段代码的对比就可以看出来 flatMap() 方法在其中起到的作用,map() 和 then() 方法也有类似的作用。但这些方法之间的区别是什么呢?我们先来看看这三个方法的签名(以 Mono 为例):

flatMap(Function> transformer)
map(Function mapper)
then(Mono other)
  • then()
    then() 看上去是下一步的意思,但它只表示执行顺序的下一步,不表示下一步依赖于上一步。then() 方法的参数只是一个 Mono,无从接受上一步的执行结果。而 flatMap() 和 map() 的参数都是一个 Function,入参是上一步的执行结果。

  • flatMap() 和 map()
    flatMap() 和 map() 的区别在于,flatMap() 中的入参 Function 的返回值要求是一个 Mono 对象,而 map 的入参 Function 只要求返回一个 普通对象。在业务处理中常需要调用 WebClient 或 ReactiveXxxRepository 中的方法,这些方法的 返回值 都是 Mono(或 Flux)。所以要将这些调用串联为一个整体 链式调用,就必须使用 flatMap(),而不是 map()。

  • 问题二:如何实现并发执行
    本段内容将涉及到如下类和方法:

方法:Mono.zip()
类:Tuple2
类:BiFunction
并发执行 是常见的一个需求。Reactive Programming 虽然是一种 异步编程 方式,但是 异步 不代表就是 并发并行 的。

在 传统的命令式编程 中,并发执行 是通过 线程池 加 Future 的方式实现的。

Future result1Future = threadPoolExecutor.submit(() -> doStep1(params));
Future result2Future = threadPoolExecutor.submit(() -> doStep2(params));
// Retrive result
Result1 result1 = result1Future.get();
Result2 result2 = result2Future.get();
// Do merge;
return mergeResult;

上面的代码虽然实现了 异步调用,但 Future.get() 方法是 阻塞 的。在使用 Reactor 开发有 并发 执行场景的 反应式代码 时,不能用上面的方式。

这时应该使用 Mono 和 Flux 中的 zip() 方法,以 Mono 为例,代码如下:

Mono item1Mono = ...;
Mono item2Mono = ...;
Mono.zip(items -> {
    CustomType1 item1 = CustomType1.class.cast(items[0]);
    CustomType2 item2 = CustomType2.class.cast(items[1]);
    // Do merge
    return mergeResult;
}, item1Mono, item2Mono);

上述代码中,产生 item1Mono 和 item2Mono 的过程是 并行 的。比如,调用一个 HTTP 接口的同时,执行一个 数据库查询 操作。这样就可以加快程序的执行。

但上述代码存在一个问题,就是 zip() 方法需要做 强制类型转换。而强制类型转换是 不安全的。好在 zip() 方法存在 多种重载 形式。除了最基本的形式以外,还有多种 类型安全 的形式:

static  Mono> zip(Mono p1, Mono p2);
static  Mono zip(Mono p1, Mono p2, BiFunction combinator); 
static  Mono> zip(Mono p1, Mono p2, Mono p3);

对于不超过 7 个元素的合并操作,都有 类型安全 的 zip() 方法可选。以两个元素的合并为例,介绍一下使用方法:

Mono.zip(item1Mono, item2Mono).map(tuple -> {
    CustomType1 item1 = tuple.getT1();
    CustomType2 item2 = tuple.getT2();
    // Do merge
    return mergeResult;
});

上述代码中,map() 方法的参数是一个 Tuple2,表示一个 二元数组,相应的还有 Tuple3、Tuple4 等。

对于两个元素的并发执行,也可以通过 zip(Mono p1, Mono p2, BiFunction combinator) 方法直接将结果合并。方法是传递 BiFunction 实现 合并算法。

  • 问题三:集合循环之后的汇聚
    本段内容将涉及到如下类和方法:

方法:Flux.fromIterable()
方法:Flux.reduce()
类:BiFunction
另外一个稍微复杂的场景,对一个对象中的一个类型为集合类的(List 、Set)进行处理之后,再对原本的对象进行处理。使用 迭代器模式 的代码很容易编写:

List subDataList = data.getSubDataList();
for (SubData item : subDataList) {
    // Do something on data and item
}
// Do something on data

当我们要用 Reactive 风格的代码实现上述逻辑时,就不是那么简单了。这里会用到 Flux 的 reduce() 方法。reduce() 方法的签名如下:

 Mono reduce(A initial, BiFunction accumulator);

可以看出,reduce() 方法的功能是将一个 Flux 聚合 成一个 Mono。

第一个参数: 返回值 Mono 中元素的 初始值。
第二个参数: 是一个 BiFunction,用来实现 聚合操作 的逻辑。对于泛型参数 中:
第一个 A: 表示每次 聚合操作 之后的 结果的类型,它作为 BiFunction.apply() 方法的 第一个入参;
第二个 ? super T: 表示集合中的每个元素的类型,它作为 BiFunction.apply() 方法的 第二个入参;
第三个 A: 表示聚合操作的 结果,它作为 BiFunction.apply() 方法的 返回值。
接下来看一下示例:

Data initData = ...;
List list = ...;
Flux.fromIterable(list)
    .reduce(initData, (data, itemInList) -> {
        // Do something on data and itemInList
        return data;
    });

上面的示例代码中,initData 和 data 的类型相同。执行完上述代码之后,reduce() 方法会返回 Mono。

消费 Mono 和 Flux(结束阶段)

直接消费的 Mono 或 Flux 的方式就是调用 subscribe() 方法。如果在 WebFlux 接口中开发,直接返回 Mono 或 Flux 即可。WebFlux 框架会完成最后的 Response 输出工作。

记参考文章

  • 参考文章1:https://www.jianshu.com/p/eef7ebe28673
  • 参考文章2:https://zhuanlan.zhihu.com/p/251238873
  • 参考文章3:https://projectreactor.io/docs/core/release/reference/
  • 参考文章4:https://www.cnblogs.com/flydean/p/13946939.html
  • 参考文章5:https://blog.csdn.net/songhaifengshuaige/article/details/79248343

你可能感兴趣的:(Soul网关框架源码学习,reactor)