概述
本文将通过对 Reactive 以及相关概念的解释引出 Spring-WebFlux,并通过一些示例向读者解释 基于 Spring-WebFlux 如何进行反应式编程实践,同时会讨论相关技术的优缺点及技术原理。
什么是 Reactive
在计算机编程领域,Reactive 一般指的是 Reactive programming。指的是一种面向数据流并传播事件的异步编程范式(asynchronous programming paradigm)
响应式编程最初是为了简化交互式用户界面的创建和实时系统动画的绘制而提出来的一种方法,但它本质上是一种通用的编程范式。
举个例子
在 Excel 里,C 单元格上设置函数 Sum(A+B),当你改变单元格 A 或者单元格 B 的数值时,单元格 C 的值同时也会发生变化。这种行为就是 Reactive
下面的例子我们用了 https://projectreactor.io/ 的 reactor 库,通过这个例子找找感觉:
FluxProcessor<Integer, Integer> publisher = UnicastProcessor.create();
publisher.doOnNext(event -> System.out.println("receive event: " + event)).subscribe();
publisher.onNext(1);
publisher.onNext(2);
// 输出
// receive event: 1
// receive event: 2
追根溯源,说到 Reactive ,就不得不提到 大名鼎鼎的响应式宣言/The Reactive Manifesto(https://www.reactivemanifesto.org),它于 2014 年发表,响应式宣言是一份构建现代云扩展架构的处方。
We believe that a coherent approach to systems architecture is needed, and we believe that all necessary aspects are already recognised individually: we want systems that are Responsive, Resilient, Elastic and Message Driven. We call these Reactive Systems.
这个框架主要使用消息驱动的方法来构建系统,在形式上可以达到弹性和韧性,最后可以产生响应性的价值。
所谓弹性和韧性,通俗来说就像是橡皮筋,弹性是指橡皮筋可以拉长,而韧性指在拉长后可以缩回原样。
解释下上面的关键词:
- 响应性 快速/一致的响应时间。假设在有 500 个并发操作时,响应时间为 1s,那么并发操作增长至 5 万时,响应时间也应控制在 1s 左右。快速一致的响应时间才能给予用户信心,是系统设计的追求。
- 韧性 复制/遏制/隔绝/委托。当某个模块出现问题时,需要将这个问题控制在一定范围内,这便需要使用隔绝的技术,避免连锁性问题的发生。或是将出现故障部分的任务委托给其他模块。韧性主要是系统对错误的容忍。
- 弹性 无竞争点或中心瓶颈/分片/扩展。如果没有状态的话,就进行水平扩展,如果存在状态,就使用分片技术,将数据分至不同的机器上。
- 消息驱动 异步/松耦合/隔绝/地址透明/错误作为消息/背压/无阻塞。消息驱动是实现上述三项的技术支撑。
- 地址透明有很多方法。例如 DNS 提供的一串人类能读懂的地址,而不是 IP,这是一种不依赖于实现,而依赖于声明的设计。再例如 k8s 每个 service 后会有多个 Pod,依赖一个虚拟的服务而不是某一个真实的实例,从何实现调用 1 个或调用 n 个服务实例对于对调用方无感知,这是为分片或扩展做了准备。
- 错误作为消息,这在 Java 中是不太常见的,Java 中通常将错误直接作为异常抛出,而在响应式中,错误也是一种消息,和普通消息地位一致,这和 JavaScript 中的 Promise 类似。
- 背压是指当上游向下游推送数据时,可能下游承受能力不足导致问题,一个经典的比喻是就像用消防水龙头解渴。因此下游需要向上游声明每次只能接受大约多少量的数据,当接受完毕再次向上游申请数据传输。这便转换成是下游向上游申请数据,而不是上游向下游推送数据。
- 无阻塞是通过 no-blocking IO 提供更高的多线程切换效率。
Reactive Programming
响应式编程是一种声明式编程范型
int a, b, sum;
a = 3;
b = 4;
sum = a + b;
a = 6;
b = 7;
System.out.println(sum);
上面是一个命令式编程的例子,先声明两个变量,然后进行赋值,让两个变量相加,得到相加的结果。但接着当修改了最早声明的两个变量的值后,sum 的值不会因此产生变化。
在 Java 9 Flow 中,按相同的思路实现上述处理流程,当初始变量的值变化,最后结果的值也同步发生变化,这就是响应式编程。这相当于声明了一个公式,输出值会随着输入值而同步变化。
SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
publisher.subscribe(new Flow.Subscriber<Integer>() {
private Integer sum = 0;
Flow.Subscription subscription = null;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(Integer item) {
subscription.request(1);
sum += item;
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
System.out.println(sum);
}
});
Arrays.asList(3, 4).stream().forEach(publisher::submit);
publisher.close();
之前有提及消息驱动,消息驱动(Message-driven)和事件驱动(Event-driven)有什么区别呢。
1) 消息驱动有确定的目标,一定会有消息的接受者,而事件驱动是一件事情希望被观察到,观察者是谁无关紧要。消息驱动系统关注消息的接受者,事件驱动系统关注事件源。
2) 在一个使用响应式编程实现的响应式系统中,消息擅长于通讯,事件擅长于反应事实。
Reactive Streams
Reactive Streams(https://www.reactive-streams.org) 提供了一套非阻塞背压的异步流处理标准,主要应用在 JVM、JavaScript 和网络协议工作中。通俗来说,它定义了一套响应式编程的标准。
有了标准,各 Reactor 库的厂商就有了规范,不再各自为战,并且对于上层应用开发者来说可以根据自己的需要选择同一个规范下的各种不同实现库。
The purpose of