异步非阻塞实现方式
回调
-
实现方式
-
回调方式会造成回调地狱
- 回调很难将逻辑组合起来,代码难以理解和维护。
A(callback(){
success(){//一层
B(callback(){
success(){//二层
C(callback(){
success(){//三层
...
}
err(e){
}
})
}
err(e){
}
})
}
err(e){
}
});
future callable completablefuture
- future
- 1.invokeall()异步并发调用,获取返回结果依然是阻塞的get(),确实可以获取异步任务的执行结果,但是获取其结果还是会阻塞调用线程的,并没有实现完全异步化处理,要么使用isDone()轮询地判断Future是否完成,这样会耗费CPU的资源。
- 2.大量的应用会写出如下代码,可读性差且不优雅
public Future<Future<T>>> xxx();
- CompletableFuture
- CompletableFuture提供了非常强大的Future的扩展功能(50多个api),可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法,完成整个异步流程的编排。
- 在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理,让主线程彻底解放出来,它避免了传统回调最大的问题,弥补了Future模式的缺点,实现了实际意义上的异步处理。
Reactive 响应式编程
- 指的是一种面向数据流并传播事件的异步编程范式
- 响应式宣言(要求,思想)
- 1.即时响应:系统在各种情况下都会尽全力保证及时响应。
- 2.回弹性:系统在面临故障时依然保持快速响应。
- 3.可伸缩性:系统在不同的负载下都能保持快速的响应。
- 4.消息驱动:响应式系统依赖异步的消息传递, 以确定各种组件的边界, 并确保松耦合、隔离性。
Reactor
- Reactor 是一个支持响应式流(Reactive Streams)的轻量级JVM基础库,是对设计模式观察者模式的扩展,用来支持对数据和事件的响应,帮助应用高效,异步地传递消息。它有非常丰富的操作符(过滤、合并、转换等)来对数据进行处理,封装一个非常好用的响应式/流式调用框架。与他相似的类库有 RxJava, RxJs, JDK9 Flow 等。
- 核心是异步非阻塞、函数式编程、事件驱动的思想。
- 实现原理
- 观察者模式
- Subject: 可观察者或者是被观察者(生产者),也叫做数据源或者发射源,数据源发射的数据通常也叫做事件。
- Observer:观察者(也叫消费者、订阅者),监听Subject发射的数据并做出响应
- Push 模式
- java8中的Stream是pull模式,下游从上游拉数据的过程,它会有中间操作例如 map filter 和 reduce,和终止操作例如 collect 等,只有在终止操作时才会真正的拉取数据。
- java中常用的数据遍历Iterator类也是一个pull拉模式来消费数据,判断是否有数据(hasNext()方法),获取数据(next())。Reactor中,数据源主动的告诉消费者发射什么事件(onNext()),事件发射什么时候结束(onComplete()),以及可能出现的报错(onError())。
Pull (Iterable) | Push (Observable) |
---|---|
T next() | onNext(T) |
throws Exception | onError(Throwable) |
return | onCompleted() |
-
反压(Backpressure)
- 反压出现的场景:上下游运行在不同的线程中,且上游发射数据速度大于下游接收处理数据的速度,也就是下游的处理能力不够了或者是Buffer出现溢出。因此Reactor中提供了Flowable这种可以设置丢弃策略的可观察者对象。
-
flux简单使用,先感受一下flux
- 被观察者对象.操作1().操作2().subscribe(观察者对象)
- Flux调用subscribe方法时,相当于subject发出了一个Event,从而让订阅此事件的观察者进行消费。
Flux.just("Tom", "Bob", "zhangsan", "lisi")
.map(s -> s.concat("@163.com"))
.filter(s -> s.length() > 10)
.subscribe(System.out::println);
- 调用链分析
Mono.just(1).map(v->{
return 200;
}).filter(v-> {
return v>=200;
}).subscribe(new Subscriber<Integer>() {
@Override
public void onSubscribe(Subscription subscription) {
}
@Override
public void onNext(Integer integer) {
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
}
1.根据每一个操作符,来创建可观察者对象,下游可观察者持有上游的引用。
2.建立上下游的订阅关系:当前可观察者对象是上游的订阅者,同时又是下游的数据源(生产者)。
3.顶层数据源开始链式onNext链接调用,每个操作符内部会执行相应的Fuction或者是 predicate(断言),最后一个操作符filter会调用生成的
lambdaObserver对象的onNext(),形成流式处理。
- 线程切换(subscribeOn publishOn)
-
Reactor默认执行数据源发射、操作符调用、订阅subscribe等一系列操作都是在主线程中同步执行的。
-
subscribeOn
- 如果我们想到把某些任务放在单独的线程执行(比如说加密消耗CPU的任务),可以使用subscribeOn。它可以指定Observable在哪个线程上发射事件,并且该线程将事件一直推送到订阅消费者手中。
- 下面示例中flatMap操作与subscribe操作的执行线程是同一个【elastic】中。
Mono.just(1).flatMap(v->{ System.out.println("flatMap:"+Thread.currentThread().getName()); return Mono.just(2); }).subscribeOn(Schedulers.elastic()).subscribe(v->{ System.out.println("subscribe:"+Thread.currentThread().getName()); });
-
publishOn
-
publishOn()操作区别于subscribeOn(),它指定的是publishOn()之前的操作是由主线程或者是subscribeOn()指定的线程来执行,而publishOn之后的操作则由publishOn指定的线程来执行。
-
下面示例中flatMap()操作执行在主线程【main】,而map操作和subscribe操作则执行在计算线程【elastic】中。
-
Mono.just(1).flatMap(v->{ System.out.println("flatMap:"+Thread.currentThread().getName()); return Mono.just(2); }).publishOn(Schedulers.elastic()).map(v->{ System.out.println("map:"+Thread.currentThread().getName()); return 3; }).subscribe(v->{ System.out.println("subscribe:"+Thread.currentThread().getName()); });
-
对比CompletableFuture和Reactor
- Reactor 3 Reference Guide中的例子:筛选出符合特定条件的一组产品 Id (findIds 方法),再通过 Id 找到对应产品名称(findProductName 方法)与成交均价(findAvgPrice 方法),最终输出统计结果。
- future实现
public CompletableFuture<List<String>> getStatisticOfFruits(){
CompletableFuture<List<String>> ids = findIds("fruits");
CompletableFuture<List<String>> result
= ids.thenComposeAsync(l -> {
Stream<CompletableFuture<String>> zip = l.stream().map(i ->{
CompletableFuture<String> nameTask = findProductName(i);
CompletableFuture<Integer> priceTask = findAvgPrice(i);
//合并二个任务执行结果
return nameTask.thenCombineAsync(priceTask,
(name, price)-> "Name: " + name + " - price: " + price);
});
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()));
});
return result;
}
- reactive实现(基于reactor库)
Flux<String> ids = rxFindIds("fruits");
Flux<String> combinations =
ids.flatMap(id -> {
Mono<String> nameTask = rxFindProductName(id);
Mono<Integer> priceTask = rxFindAvgPrice(id);
return nameTask.zipWith(priceTask,
(name, price)-> "Name " + name + " - price " + price);
});
return combinations.collectList();
}
- 再比如失败重试的编排能力CompletableFuture得手动实现,reactor可以很直观的表示重试逻辑
AtomicInteger time = new AtomicInteger(-1);
Flux.just("liu", "cheng").map(e -> {
System.out.println("时间:" + System.currentTimeMillis() + " map1处理:" + e);
return e;
}
).map(e -> {
System.out.println("map2处理:" + e);
if (time.get() <= 0 && "cheng".equals(e)) {
time.getAndIncrement();
throw new RuntimeException("这是一个错误");
}
return e.toUpperCase();
//最大重试3次,固定延迟1秒
}).retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1))).subscribe(e -> {
System.out.println(e);
});
- Reactive
- 从上面简单对比可以看出,相比 Future,基于 Reactive 模型丰富的操作符组合(filter/map/flatMap/zip/onErrorReturn/onErrorResume 过滤、合并、转换、错误处理等高阶函数)代码清晰易读,搭配 Lamda 可以轻松实现复杂业务场景任务编排
- CompletableFuture 缺点
-
1.以上二个例子可以看出难以简单优雅实现多异步任务编排;
-
2.不支持惰性计算;
- 在没有订阅者的条件下,可观察者(数据源)是不会发送事件的。
-
//mono
public Mono monoTest() {
//异步调用http,不会立即执行只有subscribe()后才会执行,有点类似只是做了一个声明
Mono mono = CallHttpWithMono();
// ... some code
if (something) {
return Mono.error(new RuntimeException());
}
return mono;
}
//CompletableFuture
public CompletableFuture futureTest() {
//会立即执行,后置的something条件判断其实没必要执行,浪费cpu io等资源
CompletableFuture future = CallHttp();
// ... some code
if (something) {
var errorFuture = new CompletableFuture();
errorFuture.completeExceptionally(new RuntimeException());
return errorFuture;
}
return future;
}
-
- 缺少与时间相关的操作
Flux.create(new Consumer<FluxSink<Integer>>() {
@Override
public void accept(FluxSink<Integer> fluxSink) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
fluxSink.next(3);
}
}).timeout(Duration.ofSeconds(2)).onErrorResume(v->{
System.out.println(v);
return Mono.just(1000);
}).subscribe();
-
多值处理的流式处理
- futureList执行的回调处理必须要等待整个List结果的返回。
- Reactor回调处理却是流式处理,先收到数据流马上执行回调,所有请求执行完成后调用一次subscribe,减少处理耗时。虽然futureList是并发执行,但是必须等所有的任务结束后才能继续执行,而先执行完的任务无法优先执行后续操作。
-
5.难以支持高级异常处理;
spring-flux
WebFlux 简介
- WebFlux 是 Spring Framework5.0 中引入的一种新的反应式Web框架。通过Reactor库实现Reactive Streams规范,完全异步和非阻塞框架。本身不会加快程序执行速度,但在高并发情况下借助异步IO能够以少量而稳定的线程处理更高的吞吐,规避文件IO/网络IO阻塞带来的线程堆积。
WebFlux特性:
-
1.webflux是一个异步非阻塞的Web框架,它能够充分利用多核CPU的硬件资源去处理大量的并发请求
-
2.内部使用的是响应式编程,以Reactor库为基础,基于异步和事件驱动,可以让我们在不扩充硬件资源的前提下,提升系统的吞吐量和伸缩性。
-
3.不能使接口的响应时间缩短,它仅仅能够提升吞吐量。
WebFlux的设计目标
- 适用高并发
- 高吞吐量
为什么spring-flux要比springmvc快
-
Spring MVC 构建于 Servlet API 之上,使用的是同步阻塞式 I/O 模型,每一个请求对应一个线程去处理。
-
Spring WebFlux 是一个异步非阻塞式 IO 模型,核心是基于 Reactor 的API实现的,通过少量的容器线程就可以支撑大量的并发访问,所以 Spring WebFlux 可以有效提升系统的吞吐量和伸缩性,特别是在一些 IO 密集型应用中,Spring WebFlux 的优势明显。例如微服务网关 Spring Cloud Gateway 就使用了 WebFlux,这样可以有效提升对下游服务的吞吐量。
-
NIO+Reactor
- spring-flux+webclient的线程模型
- 分request和response2个异步过程
- spring-flux+webclient的线程模型