文章目录
什么是Mono/Flux
反应式编程框架主要采用了观察者模式,而SpringReactor的核心则是对观察者模式的一种衍生。
理解Mono和Flux,也可以理解为Publisher(发布者也可以理解为被观察者)主动推送数据给Subscriber(订阅者也可以叫观察者),如果Publisher发布消息太快,超过了Subscriber的处理速度,如何处理。这时就出现了Backpressure。
这就和上一文再临SpringBoot——Reactive Stream联系了起来。
再来具体理解,在Reactor中,经常使用的类并不多,主要有以下两个:
- Mono 实现了 org.reactivestreams.Publisher 接口,代表0到1个元素的发布者。(可以为0,类似于Optional)
- Flux 同样实现了 org.reactivestreams.Publisher 接口,代表0到N个元素的发布者。(类似Stream)
看看下面一段代码:
public class Test {
public static void main(String[] args) {
Subscriber subscriber=new Subscriber() {
...
};
String[] strs={"1","2","3"};
//java8 stream
Flux.fromArray(strs).map(s->Integer.parseInt(s))
//java9 reactive stream
.subscribe(subscriber);
}
}
其实 Mono/Flux就是java8的stream加上java9的reactive stream的实现,是一个发布订阅模式的衍生。
下面这段代码:
@RestController
@Slf4j
public class HomeController {
@GetMapping("/get1")
public String get1(){
return "get1";
}
@GetMapping("/get2")
public Mono<String> get2() {
return Mono.just("get2");
}
@GetMapping("/get3")
public String get3() throws InterruptedException {
log.info("get3 start");
String result=createStr();
log.info("get3 end");
return result;
}
@GetMapping("/get4")
public Mono<String> get4(){
log.info("get4 start");
Mono<String> result= Mono.fromSupplier(()-> {
try {
return createStr();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "error";
});
log.info("get4 end");
return result;
}
private String createStr() throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
return "get_sleep5";
}
}
get1和get2在直观上没有什么不同,如果改成get3和get4这样,接一个比较耗时的方法。我们看看运行结果:
get3:
2019-09-26 16:36:10.234 INFO 8900 --- [ctor-http-nio-2] c.l.demo.controller.HomeController : get3 start
2019-09-26 16:36:15.239 INFO 8900 --- [ctor-http-nio-2] c.l.demo.controller.HomeController : get3 end
get4:
2019-09-26 16:37:58.941 INFO 8900 --- [ctor-http-nio-2] c.l.demo.controller.HomeController : get4 start
2019-09-26 16:37:58.943 INFO 8900 --- [ctor-http-nio-2] c.l.demo.controller.HomeController : get4 end
直接观察可以看到,get3在HomeController中,占用了5秒的时间。而get4几乎是立即完成了。(注意:在前端的感受都是5秒)。
所以,使用Mono/Flux能够极大的增加后端接受请求的数目。
@GetMapping(value = "/flux", produces = "text/event-stream")
public Flux<String> flux() {
Flux<String> flux = Flux.fromStream(
IntStream.range(1, 5)
.mapToObj(i->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "flux data -- "+i;
}));
return flux;
}
上面这段代码通过produces = "text/event-stream"能验证后端像流一样,依次返回数据,而不是统一处理后一起返回。
更多知识,可以查看Reactor核心特性
调度器
与RxJava一样,Reactor 也可以被视为与并发无关的,也就是说,它不强制执行并发模型。相反,它让开发人员掌握主动权。你可以使用一些库帮你实现并发性。在Reactor中,执行模型和执行发生的位置由使用的Scheduler决定的,Scheduler是一个可以有广泛实现的接口。Schedulers 类具有静态方法,可以访问以下执行上下文:
- 当前线程 (Schedulers.immediate()).
- 一个单例,可以重复使用的线程 (Schedulers.single()). 注意,此方法对所有调用者重用相同的线程,直到scheduler被处理掉。如果你想每次调用都有一个专用的线程可以使用 Schedulers.newSingle()
- 一个弹性的线程池 (Schedulers.elastic())。其中工作线程在空闲一段时间(默认60s)后会被回收。对于I/O阻塞工作来说,这是一个不错的选择。Schedulers.elastic() 是一个为阻塞操作提供专用线程的便利的方式,这样它就不会去占用其他资源。【怎么去包装一个同步阻塞调用】。
- 一个可以并行工作的固定的线程池 (Schedulers.parallel()),它会根据你的CPU核心数来创建线程数量。
先来看看下面这个代码:
public class Test {
public static void main(String[] args) {
println("hello world");
Flux.just("A","B","C")
.subscribe(Test::println);
}
public static void println(Object object){
String processor=Thread.currentThread().getName();
System.out.println("[当前线程: "+processor+"] "+object);
}
}
Flux.just就是一次处理ABC三个字符串,因为Flux没有forEach方法,所以使用subscribe 进行数据发布。
其输出为:
[当前线程: main] hello world
16:25:26.235 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
[当前线程: main] A
[当前线程: main] B
[当前线程: main] C
可以看到,目前都是在主线程中跑的。
Flux.just("A","B","C")//发布A->B->C
.publishOn(Schedulers.elastic())//线程切换
.subscribe(Test::println);
输出:
[当前线程: main] hello world
16:56:59.199 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
[当前线程: elastic-2] A
[当前线程: elastic-2] B
[当前线程: elastic-2] C
再来看下面这一段:
public class Test {
public static void main(String[] args) throws InterruptedException {
println("hello world");
Flux.just("A","B","C")//发布A->B->C
.publishOn(Schedulers.elastic())//线程切换
.map(value->"+"+value)
.subscribe(Test::println,//数据消费
Test::println,//异常处理
()->println("完成操作"));//完成回调
Thread.sleep(1000L);
}
public static void println(Object object){
String processor=Thread.currentThread().getName();
System.out.println("[当前线程: "+processor+"] "+object);
}
}
输出:
[当前线程: main] hello world
17:02:16.915 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
[当前线程: elastic-2] +A
[当前线程: elastic-2] +B
[当前线程: elastic-2] +C
[当前线程: elastic-2] 完成操作
我在主线程中设置了sleep方法,因为主线程还没有退出,所以有一个完成操作的回调。如果没有sleep方法,那么主线程在elastic-2线程之前就退出了,可能不会显示完成操作的输出。
下面这个例子更好的诠释了,Flux是java8的stream和java9的reactive stream的组合:
public class Test {
public static void main(String[] args) {
println("hello world");
Flux.just("A", "B", "C")//发布A->B->C
.publishOn(Schedulers.elastic())//线程切换
.map(value -> "+" + value)
.subscribe(Test::println,//数据消费 =OnNext()
Test::println,//异常处理 =OnError(Throwable)
() -> println("完成操作"),//完成回调 =OnComplete
subscription -> {//背压操作 =OnSubscribe(Subscription)
subscription.request(2);
subscription.cancel();
}
)
;
// Thread.sleep(1000L);
}
public static void println(Object object) {
String processor = Thread.currentThread().getName();
System.out.println("[当前线程: " + processor + "] " + object);
}
}