Reactor 3 Reference Guide - 选译 (1)

Reactor是Java 8上的响应式编程框架,支持背压,与Java 8函数式API和Reactive Streams兼容。本文介绍了从阻塞到非阻塞编程的转变,阐述了异步编程的优势,详细讲解了Reactor中的Flux和Mono,以及如何创建和订阅这些序列。文章还涵盖了背压、组合性、可读性、Operators和订阅的重要性。
摘要由CSDN通过智能技术生成

About the Documentation

本节简要概述了Reactor参考文档。不需要一行一行地阅读本指南,每一节都是独立的,尽管他们经常互相引用。

Getting Started

Introducing Reactor

Reactor是JVM上的完全非阻塞的响应式编程框架,支持有效的需求管理(通过背压的方式)。它直接与Java 8的函数式API集成,特别是CompletableFuture、Stream和Duration。它提供了可组合的异步序列(asynchronous sequence)API:Flux(用于[N]个元素)和Mono(用于 [0|1]个元素)。广泛实现了Reactive Streams
使用reactor-netty,Reactor也支持非阻塞的进程间通信。配合微服务架构,Reactor Netty为HTTP(包括Websockets)、TCP和UDP提供了背压的网络引擎。完全支持响应式的Encoding和Decoding。

Prerequisites

Reactor Core最低支持Java 8。
它依赖org.reactivestreams:reactive-streams:1.0.2。

Android支持:

  • Reactor 3没有正式支持Android(RxJava 2支持)
  • 在Android SDK 26以上可以工作得很好

Introduction to Reactive Programming

Reactor是响应式编程范式的一个实现,可以概括为:
响应式编程是关于数据流和变化传播(the propagation of change)的异步编程范式。这意味着可以通过采用的编程语言轻松地表达静态(比如数组)或者动态(比如事件发送器)的数据流。
Reactive_programming

Microsoft最先在.NET ecosystem增加了Reactive Extensions(Rx)库。然后,RxJava在JVM上实现了响应式编程。后来,通过Reactive Streams实现了Java上的标准化-Flow类定义的接口集和交互规则集成到了Java 9内。
响应式编程范式通常在面向对象的语言中出现,是Observer设计模式的扩展。比较一下响应式流和Iterator设计模式,一个主要不同点是,Iterator是pull-based的,而响应式流是push-based。
使用一个迭代器是一种命令式的编程模式,尽管访问值的方法完全是Iterable的责任。开发人员可以选择何时访问序列中的next()项。
而响应式流,是发布-订阅模式的。发布者提醒订阅者来了新值,这种push是响应式的关键。除了推送值,还以明确定义的方式,涵盖错误处理和完成。通过调用onNext,发布者将新值推送给订阅者。也可以通过调用onError发送一个错误信号,或者通过onComplete完成。错误和完成都会终止序列,可以概括为:

onNext x 0..N [onError | onComplete]

这种方法非常灵活,它支持没有值、一个值和n个值(包括无限的值序列,比如时钟的连续滴答)。

但是,为什么需要一个异步的响应式库呢?

Blocking Can Be Wasteful

现代程序可以覆盖大量并发用户,虽然硬件性能在持续提升,可软件性能还是一个关键问题。
有两种办法提高程序性能:

  • parallelize:采用更多线程和更多硬件资源
  • seek more efficiency:寻求当前资源的更高效率

一般来说,Java程序都是阻塞式的。容易达到性能瓶颈,就需要更多线程,运行类似的阻塞代码。这样做,很快就会出现争用和并发问题。
更糟糕的是,阻塞浪费资源。如果仔细观察,一旦程序涉及延迟(特别是I/O,比如数据库操作和网络调用),资源就会被浪费-因为线程处于空闲或者等待数据的状态。

Asynchronicity to the Rescue?

也可以编写异步的、非阻塞的代码,使用相同的底层资源切换到其他活动任务,等异步处理执行完成再切换回来。
在JVM内,怎么写异步代码呢?

  • Callbacks:异步方法没有返回值,但是需要额外的回调参数(lambda或者匿名类),结果有效时就调用该参数。比如Swing中的EventListener
  • Futures:异步方法立即返回Future。异步过程计算T的值,Future对象包装了对值的访问。该值不会立即有效,值有效以后才可以拉取(poll)。比如运行Callable任务的ExecutorService使用Future对象

这两种方法都有局限性。
Callbacks很难组合到一起。阅读和维护起来也很困难。

From Imperative to Reactive Programming

诸如Reactor这样的响应式库不但解决上述缺点,还关注其他方面:

  • 组合性和可读性
  • 数据作为流(flow),有丰富的operators
  • subscribe前什么都没发生
  • Backpressure,消费者向生产者发送明确的信号,表明生产得太快了
  • 与并发无关的高级抽象

Composability and Readability

composability是指有能力编排多个异步任务,前一个任务的结果就是后续任务的输入,或者使用fork-join执行几个任务,也可以重用任务,把它作为更高级系统的组件。
编排任务的能力与代码的可读性和可维护性紧密相关。随着异步过程层级的数量和复杂性的增加,编写和阅读代码都变得越来越难。正如我们所看到的,回调很简单,但它的一个主要缺点是,对于复杂的过程,你需要在回调中执行回调,他们嵌套在一起(Callback Hell)。
Reactor提供了丰富的组合选项,代码反应了抽象过程的组织,全都位于同一级(嵌套最小化)。

The Assembly Line Analogy

你可以想象为,响应式程序里的数据在装配线上移动。Reactor既是传送带,又是工作站。原材料从源注入(Publisher),最终的成品推送给消费者(Subscriber)。
原材料经历各种转换和其他中间步骤,或者是大型装配线上的一部分和其他部件聚合到一起。如果在某一点出现了故障或者堵塞(也许花费了太长时间),受影响的工作站可以向上游信号以限制原材料的流动。

Operators

Reactor中,operators就是装配线类比中的工作站。每个operator都会向Publisher添加行为,并把前一步的Publisher包装成新的实例。整个链就这样形成了,数据从第一个Publisher沿着链向后移动,由每个link转发。最终,Subscriber完成了该过程。记住,在Subscriber订阅Publisher前,什么都没发生。
Reactive Streams规范根本没有指定任何operators,Reactor的最佳附加值就是添加了丰富的operators。他们涉及很多方面,从简单的转换、过滤到复杂的编排和错误处理。

Nothing Happens Until You subscribe()

Reactor中,当你写一个Publisher链,默认情况下,数据不会启动。你要增加一个异步处理的抽象描述(帮助重用和组合)。
通过订阅,把Publisher绑定到Subscriber,从而出发整个链中的数据流。在内部,Subscriber发送一个request信号,向上游传递,直到Publisher。

Backpressure

backpressure也是通过向上游传递信号实现的,还是用装配线做类比,如果工作站处理得比上游慢就发送一个反馈信号。
Reactive Streams规范的定义非常接近类比:subscriber可以在unbounded模式工作,让源以最快的速度推送数据;或者使用request机制,向源发信号,它现在可以处理最多n条数据。
中间operators也可以改变request。比如buffer operator,可以把数据分组。还有些operators实现了prefetching策略,这就避免了request(1)往返,如果在请求之前就生成元素不太昂贵,这样处理是划算的。
这样,把push模式变成了push-pull,如果有数据,下游可以从上游pull数据。如果没有数据,就等数据准备好以后push给下游。

Hot vs Cold

有两大类反应式序列hot和cold。主要区别是响应式流如何应答订阅:

  • Cold:为每个Subscriber都生成新的序列,包括数据源。比如,如果源包装了一个HTTP调用,就为每个订阅生成一个新的HTTP请求
  • Hot:对于每个Subscriber,不会重新开始。相反,迟到的订阅者只能接收到订阅之后发射的数据。注意,一些hot响应式流可以缓存或者重放历史(甚至全部历史)。hot的序列甚至可以在没有订阅者时也发射数据

Reactor Core Features

Reactor提供了可组合的响应式类型,他们(Flux和Mono)实现了Publisher,还提供了丰富的operators。Flux代表0…N个元素,Mono代表(0…1)。
比如,HTTP请求只有一个响应,所以应该不会做count运算。所以,使用Mono代表一次HTTP请求的结果会更好。
改变最大基数的Operators会切换相关类型。比如,Flux才有count运算,但是它返回Mono。

Flux, an Asynchronous Sequence of 0-N Items

Flux

Flux是一个标准的Publisher,可以由一个completion信号或者error终止。三种类型的信号转换为对下游Subscriber的onNext、onComplete或者onError方法的调用。
所有的事件,包括terminating,都是可选的。如果没有onNext事件但是有onComplete代表一个empty有限序列;而删除了onComplete,就变成一个无限的空序列(没什么用,除非要测试cancellation)。
无限序列不一定是空的,比如,Flux.interval(Duration)生产的Flux就是无限的,根据时钟发出滴答声。

Mono, an Asynchronous 0-1 Result

Mono

Mono 是一个专用的Publisher,最多发送一条数据,然后可以由onComplete或者onError信号终止。
只包含Flux的operators的子集,一些operators可以切换到Flux。
比如,Mono#concatWith(Publisher)返回一个Flux,Mono#then(Mono) 返回另一个Mono。
Mono可以代表一个无值的异步处理,它仅有completion概念(类似Runnable)。想增加这样一个,请使用Mono。

Simple Ways to Create a Flux or Mono and Subscribe to It

可以使用工厂方法开始使用Flux和Mono。
比如,要增加一个String序列,可以枚举他们,可以放进集合,然后增加Flux:

Flux<String> seq1 = Flux.just("foo", "bar", "foobar");

List<String> iterable = Arrays.asList("foo", "bar", "foobar");
Flux<String> seq2 = Flux.fromIterable(iterable);

其他例子:

//没有值,也可以使用泛型
Mono<String> noData = Mono.empty(); 

Mono<String> data = Mono.just("foo");
//第一个参数是范围的开始,第二个参数是数量
Flux<Integer> numbersFromFiveToSeven = Flux.range(5, 3); 

订阅的时候,Flux和Mono支持Java 8 lambdas。可以选择.subscribe()的变种,将lambdas用于不同的回调组合:

//订阅,触发一个序列
subscribe(); 
//使用每个产生的值做点啥
subscribe(Consumer<? super T> consumer); 
//处理值,也响应错误
subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer); 
//处理值和错误。当序列successfully完成时,做点啥
subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer,
          Runnable completeConsumer); 
//处理值、错误和successful完成。再使用Subscription做点什么
subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer,
          Runnable completeConsumer,
          Consumer<? super Subscription> subscriptionConsumer);

这些变种返回对subscription的引用,当你不再需要数据时,可以cancel该subscription。通过cancellation,源会停止生产值,清除它增加的资源。这种cancel和清理由Disposable接口代表。

subscribe Method Examples

本节包含subscribe方法的每个签名的最小示例。
先是无参方法:

//生产三个值
Flux<Integer> ints = Flux.range(1, 3);
//订阅
ints.subscribe();

前面的代码没有产生可见的输出,但是它能工作。该Flux产生了三个值。如果我们提供一个lambda,可以让值可视:

Flux<Integer> ints = Flux.range(1, 3); 
//订阅,打印值
ints.subscribe(i -> System.out.println(i));

输出是:

1
2
3

为了演示下一个方法签名,我们故意引入错误:

//产生四个值
Flux<Integer> ints = Flux.range(1, 4) 
//需要map处理不同的值
      .map(i -> { 
          //对于大多数值,返回该值
        if (i <= 3) return i; 
        //强制产生错误
        throw new RuntimeException("Got to 4"); 
      });
//订阅包含了错误处理
ints.subscribe(i -> System.out.println(i), 
      error -> System.err.println("Error: " + error));

我们有两个lambda表达式,一个为期望的内容,一个为错误。输出是:

1
2
3
Error: java.lang.RuntimeException: Got to 4

subscribe的下一个签名包含completion事件的处理:

Flux<Integer> ints = Flux.range(1, 4); 
ints.subscribe(i -> System.out.println(i),
    error -> System.err.println("Error " + error),
    () -> System.out.println("Done")); 

error信号和completion信号都是终止事件,彼此排斥(不可能同时得到)。要使completion消费者工作,就不能触发错误。
completion回调没有输入,由一对空括号表示:它匹配Runnable接口的run方法。前面代码的输出是:

1
2
3
4
Done

subscribe方法的最后一个签名包含一个Consumer。可以使用Subscription做些事情:执行request(long),或者cancel()。否则Flux会被挂起:

Flux<Integer> ints = Flux.range(1, 4);
ints.subscribe(i -> System.out.println(i),
    error -> System.err.println("Error " + error),
    () -> System.out.println("Done"),
    //源最多发射10个元素,实际上,只发射了四个
    sub -> sub.request(10)); 

Cancelling a subscribe() with its Disposable

subscribe()的这些变种都有一个Disposable返回类型。在这里,Disposable代表订阅能被取消(通过调用dispose() 方法)。
对于Flux和Mono,cancellation是一个信号,源会停止产生数据。但是,不能保证是立竿见影的。有些源生产数据的速度太快,在收到cancel指令前可能已经完成了。
Disposables类中有一些实用的工具。Disposables.swap()增加一个Disposable包装器,允许你原子地cancel或者替换一个具体的Disposable。比如在一个UI场景中,每当用户按下一个button,你就可以cancel一个请求,替换成一个新的。

Alternative to lambdas: BaseSubscriber

也可以扩展BaseSubscriber,实现订阅功能。
比如这样调用一个SampleSubscriber:

SampleSubscriber<Integer> ss = new SampleSubscriber<Integer>();
Flux<Integer> ints = Flux.range(1, 4);
ints.subscribe(i -> System.out.println(i),
    error -> System.err.println("Error " + error),
    () -> {System.out.println("Done");},
    s -> s.request(10));
ints.subscribe(ss);

SampleSubscriber是这样实现的:

public class SampleSubscriber<T> extends BaseSubscriber<T> {

	public void hookOnSubscribe(Subscription subscription) {
		System.out.println("Subscribed");
		request(1);
	}

	public void hookOnNext(T value) {
		System.out.println(value);
		request(1);
	}
}

SampleSubscriber类扩展了BaseSubscriber,这是自定义Subscribers时,Reactor推荐扩展的抽象类。它提供了可以被覆盖的钩子,以调整subscriber的行为。默认会触发一个unbounded的请求,表现得很像subscribe()。如果你想自定义请求总量,扩展BaseSubscriber就很好。
要自定义请求量,最低限度要实现hookOnSubscribe(Subscription subscription) 和hookOnNext(T value)。前面的例子,hookOnSubscribe打印到标准输出,然后发送第一次请求。hookOnNext打印值,执行附加的请求,每次一条。
上面SampleSubscriber类的输出是

Subscribed
1
2
3
4

BaseSubscriber类还包含requestUnbounded()方法,可以切换到unbounded模式(相当于request(Long.MAX_VALUE))。此外还有cancel()方法。
它还有这些钩子:hookOnComplete、hookOnError、hookOnCancel和hookFinally(总是在序列终止时被调用,终止类型见SignalType参数)。

On Backpressure, and ways to reshape requests

Reactor实现背压的时候,消费者向上游operator发送request。当前请求的和有时候被称为当前demand或者是pending request。上限是Long.MAX_VALUE,代表无限的请求(没有背压)。
第一个请求来自最终的subscriber。在订阅的时候,最直接的办法是触发无限的请求:

  • subscribe()和大多数变种
  • block()、blockFirst()和blockLast()
  • 使用toIterable()/toStream()迭代

自定义原始请求的最简单的办法是覆盖BaseSubscriber的hookOnSubscribe方法:

Flux.range(1, 10)
    .doOnRequest(r -> System.out.println("request of " + r))
    .subscribe(new BaseSubscriber<Integer>() {

      @Override
      public void hookOnSubscribe(Subscription subscription) {
        request(1);
      }

      @Override
      public void hookOnNext(Integer integer) {
        System.out.println("Cancelling after having received " + integer);
        cancel();
      }
    });

输出是

request of 1
Cancelling after having received 1

操纵request的时候,你必须小心地产生足够的序列要求,否则你的Flux会被卡住。所以,BaseSubscriber的hookOnSubscribe默认是无限的request。如果你覆盖这个钩子,最少要调用一次request。

Operators changing the demand from downstream

要记住,subscribe级别的要求,能被上游的每个operator重新整形。比如buffer(N) operator:如果它收到request(2),它解释成需要two full buffers。因为buffers认为有N个元素就是满的,该buffer operator使得request成了2 x N。

Prefetch是在内部序列调整初始request的方法,一般来说,默认是32。
这些operators一般也实现了补充(replenishing)优化:一旦operator看到25%的prefetch已经完成,就再向上游请求25%。这是一种启发式的优化,让这些operators主动预测即将到来的请求。

也可以直接调整request:limitRate和limitRequest。
limitRate(N)拆分下游请求,让他们以较小的批量传播到上游。比如一个100的request,通过limitRate(10)会导致最多10次10个的requests,传播到上游。limitRate实现了上面讨论的补充优化。
该operator有个变种,可以调整补充总量:limitRate(highTide, lowTide),lowTide为0就是严格的highTide批量的请求,而没有补充优化。
limitRequest(N)定义下游request的最大总需求。如果单个request不会让总需求超过N,整个request就会传给上游。达到总量,limitRequest认为序列完成,向下游发射onComplete并cancel资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值