为什么需要响应性编程?
先来看看传统的Web应用有哪些缺点。
Thread per Request模型
比如使用Servlet开发的单体应用,部署到Tomcat,Tomcat有线程池,每个请求过来后都会交给线程池中的一个线程来执行,这就是Thread per Request模型,如果执行过程中包括访问数据库,或者包括读取文件,也就是IO操作,则在进行IO操作时,请求线程是阻塞的,即使是阻塞线程也是占用资源的,典型的每个线程要使用1MB的内存,而且这个线程不能处理其他请求。
如果有并发请求,则会同时有多个线程处于阻塞状态,每个线程占据一份资源。同时,Tomcat的线程池大小决定了可以同时处理多少个请求。这就意味着应用仅能处理并发数为线程池大小的请求。如果要处理更多的请求,就需要配置更大的线程池,但是线程占用内存(一般一个线程1MB的样子),线程数越多,占用的内存越大;同时线程越多又会带来线程上下文切换的消耗。
因此,当并发数很大的时候,Thread per Request模型很消耗资源。
等待I/O操作
在I/O操作中也存在大量的资源浪费:如调用数据库,读取文件等。
此时,发出I/O请求的线程会阻塞等待I/O操作的完成,即阻塞式I/O。这些线程的阻塞仅仅是为了等待一个响应,浪费了线程,浪费了内存。
响应延迟
传统命令式编程另一个问题是:当一个服务需要做很多操作而不仅仅是I/O请求的时候,响应延迟相应的增大。
如服务A需要调用服务B和C,比如查询数据库,聚合结果并返回。意味着服务A的响应时间包括:
- 服务B的响应时间(网络延迟时间+处理时间)
- 服务C的响应时间(网络延迟时间+处理时间)
- 数据库请求响应时间(网络延迟时间+处理时间)
如果服务调用没有前后依赖关系,则可以并行调用服务。如果使用java的CompletableFuture异步调用并注册回调,开发会复杂很多,而且阅读和维护也会复杂很多。
压垮客户端
微服务的另一个问题是:服务A请求服务B的数据,如果数据量很大,超过了服务A能处理的程度,则导致服务OOM。
总结
上述的问题就是响应式编程要解决的。
响应式编程的优势:
- 不用Thread per Request模型,使用少量线程即可处理大量的请求。
- 在执行I/O操作时不让线程等待。
- 简化并行调用。
- 支持背压,让客户端告诉服务端它可以处理多少负载。
响应式流规范
响应式流规范的说明
响应式流规范网址:http://www.reactive-streams.org
响应式流的规范文档:https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.3/README.md
响应式规范发布了一组接口,用于实现。
maven仓库地址:
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams-tck</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams-tck-flow</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams-examples</artifactId>
<version>1.0.3</version>
</dependency>
响应式流(Reactive Streams)规范,规定了异步组件之间使用背压进行交互。
响应式流在Java 9中使用Flow API适配。Flow API是互操作的规范,而不是具体的实现,它的语义跟响应式流规范一致。
响应式流规范的接口规范
Publisher表示数据流的生产者或数据源,包含一个方法让订阅者注册到发布者,Publisher代表了发布者和订阅者直接连接的标准化入口点。
public interface Publisher<T> {
public void subscribe(Subscriber<? super T> s);
}
Subscriber:表示消费者,onSubscribe方法为我们提供了一种标准化的方式来通知Subscriber订阅成功。
public interface Subscriber<T> {
public void onSubscribe(Subscription s);
public void onNext(T t);
public void onError(Throwable t);
public void onComplete();
}
- onSubscribe:发布者在开始处理之前调用,并向订阅者传递一个订阅票据对象(Subscription)。
- onNext:用于通知订阅者发布者发布了新的数据项。
- onError:用于通知订阅者,发布者遇到了异常,不再发布数据事件。
- onComplete:用于通知订阅者所有的数据事件都已发布完。
Subscription:onSubscribe方法的传入参数引入一个名为Subscription(订阅)的订阅票据。Subscription为控制元素的生产提供了基础。
public interface Subscription {
public void request(long n);
public void cancel();
}
- request:用于让订阅者通知发布者随后需要发布的元素数量。
- cancel:用于让订阅者取消发布者随后的事件流。
Processor:如果实体需要转换进来的项目,并将转换后的项目传递给另一个订阅者,此时需要Processor接口。
该接口既是订阅者,又是发布者:
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}
所有接口都存在于org.reactivestreams包中:
JDK9中同样有这些接口。
底层机制如下图:
响应式规范实战
这里使用的是JDK9中的API。
Publisher+Subscriber的使用
package com.morris.spring.webflux.reactivestream;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* Publisher+Subscriber的使用
*/
public class PublisherSubscriberDemo {
public static void main(String[] args) throws InterruptedException {
// 创建订阅者
Flow.Subscriber<Integer> subscriber = new Flow.Subscriber<>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(Integer item) {
System.out.println("item: " + item);
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("onComplete");
}
};
// 创建发布者
SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
// 发布者与订阅者建立关系
publisher.subscribe(subscriber);
// 发布消息
IntStream.range(0, 100).boxed().forEach(t -> publisher.submit(t));
// 休眠1s,防止主程序退出时,publisher中的线程还没启动,导致发布订阅的逻辑不会执行
TimeUnit.SECONDS.sleep(1);
}
}
Processor的使用
package com.morris.spring.webflux.reactivestream;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* Processor的使用
* Processor既是Publisher,又是Subscriber
*/
public class ProcessorDemo {
public static void main(String[] args) throws InterruptedException {
// 发布者
SubmissionPublisher<Integer> publisher = new SubmissionPublisher();
// processor
Flow.Processor<Integer, String> processor = new Flow.Processor<>() {
private Flow.Subscription subscription;
private Flow.Subscriber<? super String> subscriber;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscriber.onSubscribe(subscription);
}
@Override
public void onNext(Integer item) {
subscription.request(1);
subscriber.onNext("item" + item);
}
@Override
public void onError(Throwable throwable) {
subscriber.onError(throwable);
}
@Override
public void onComplete() {
subscriber.onComplete();
}
@Override
public void subscribe(Flow.Subscriber<? super String> subscriber) {
this.subscriber = subscriber;
}
};
// 创建订阅者
Flow.Subscriber<String> subscriber = new Flow.Subscriber<>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(String item) {
System.out.println(item);
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("onComplete");
}
};
// 建立publisher与processor的关系
publisher.subscribe(processor);
// 建立processor与subscriber的关系
processor.subscribe(subscriber);
IntStream.rangeClosed(1, 100).boxed().forEach(t -> publisher.submit(t));
// 休眠1s,防止主程序退出时,publisher中的线程还没启动,导致发布订阅的逻辑不会执行
TimeUnit.SECONDS.sleep(1);
}
}