反应式编程的认识
基础认识
命令式编程:后一行代码需要等待前一行代码执行完毕,也就是后面的执行任务步骤依赖于前面的执行任务。
反应式编程: 定义了一组如何处理数据的任务,这些任务是可以并行进行的,可以在处理数据的一部分子集的完成之后,就立马将这部分数据子集传递给下一个任务,同时继续处理另外的数据子集。
命令式编程的理念很简单:你可以一次一个的按照顺序将代码编写为需要遵循的指令列表,在某项指令开始执行之后,程序在开始下一项任务之前需要等待当前指令的完成。在整个处理数据的过程中,需要处理的数据都必须是完全可用的,它们是做作为一个整体看待的。
反应式编程本质上是函数式和声明式的,相对于要求将需要被处理的数据看作是一个整体来看待,反应流可以在数据可用的时候立即开始处理;相对于描述一组将一次执行的步骤,反应式编程描述了数据将会流经的管道或者流。
反应式流规范
反应式流是一种规范,旨在提供无阻塞回压的异步流处理标准。
反应式流可以总结为四个接口Publisher,Subscriber,Subscription,Processor。Publisher负责生成数据,Subscriber负责接收数据,Subscription是描述订阅关系的。 Processor是Publisher和Subscriber的结合,既可以是Publisher也可以是Subscriber。
public interface Publisher<T> {
// 传入一个Subscriber对象,表示Subscriber订阅这个Publisher的事件
public void subscribe(Subscriber<? super T> s);
}
public interface Subscriber<T> {
// Publisher调用onSubscribe方法,会将Subscription对象传给Subscriber,Subsciber通过Subscription来管理订阅情况
public void onSubscribe(Subscription s);
// Publisher发布的每个数据都会通过onNext方法传给Subscriber,如果有错误就会调用onError方法,如果Publisher没有更多的数据了,就会调用onComplete方法。
public void onNext(T t);
public void onError(Throwable t);
public void onComplete();
}
public interface Subscription {
// Subscriber调用request方法来向Publisher请求发送数据,参数是用来表明能接收多少的数据,回压发挥作用的地方,Publisher发布的数据都会通过onNetx方法传递给Subscriber对象
public void request(long n);
// Subscriber调用cancel方法来取消订阅
public void cancel();
}
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}
publier就是看作一个事件发布者,subscribe就是订阅这个事件的订阅者。先调用Publisher对象的subscribe方法,将Subscriber对象作为方法参数传进去,当Publisher开始发布事件的时候,会调用持有的Subscriber对象的onSubscribe方法,将Subscription对象传进去,这样Subscriber对象就有了subscription对象,Subscriber对象可以调用subscription的request方法去请求指定数量的数据,publisher会通过他所持有的Subscriber对象去调用onNetx方法,将数据传递给subscriber,如果有了错误就会调用onError方法,如果没有数据了就会调用onComplete方法,来告诉Subscriber对象数据发送完毕了。
Reactor
Reactor是反应式流规范的一种具体实现。
public void test1() {
String s1 = "zrl";
String upperCase = s1.toUpperCase();
String union = "Hello, " + upperCase;
System.out.println(union);
}
public void test2() {
Mono.just("zrl")
.map(x -> x.toUpperCase())
.map(y -> "hello, " + y)
.subscribe(System.out::println);
}
使用命令式编程模型,每行代码执行一个步骤,并且肯定在同一个线程中执行,每一步在执行完成之前都会阻塞执行线程执行下一步。
第二个方法中则不同,虽然看起来保持着按步骤执行的模型,但实际是数据会流经处理管线,在处理管线中,不能判断是在哪一个线程中执行操作,有可能在同一个线程中也可能在不同的线程中。
反应式操作
Flux和Mono是Reactor提供的最基础的构建块,这两个都实现了Publisher接口,Flux代表零个,一个或者多个数据项的管道,Mono是一个特殊的反应式类型,针对数据项不超过一个的场景。(StepVerifier:验证管道数据的类)
Flux和Mono共有500多个操作,这些操作可以大致归类为:
- 创建操作
- 组合操作
- 转换操作
- 逻辑操作
创建操作:
// 使用固定值
Flux<String> f1 = Flux.just("1","2","3","4","5","6","7");
// 使用数组
Flux<String> f2 = Flux.fromArray(new String[]{"1","2","3","4","5","6","7"});
// 使用Iterable
Set<String> s1 = new HashSet<>();
Flux<String> f3 = Flux.fromIterable(s1);
// stream
Flux<String> f5 = Flux.fromStream(s1.stream());
// 使用 Supplier<Stream<String>>
Flux<String> f6 = Flux.fromStream(() -> s1.stream());
// 使用Publisher(Mono 和 Flux 都实现了Publisher接口)
Flux<String> f7 = Flux.from(f1);
Flux<String> f8 = Flux.from(Mono.just("123"));
just,fromArray,fromIterable,fromStream;range,range是一个生成计时的方法,interval也是一个生成数字的,但是可以指定间隔时间。
组合操作:
List<String> weekStrList = new ArrayList<>();
weekStrList.add("周一");
weekStrList.add("周二");
weekStrList.add("周三");
weekStrList.add("周四");
weekStrList.add("周五");
weekStrList.add("周六");
weekStrList.add("周日");
List<Integer> weekNumList = new ArrayList<>();
weekNumList.add(1);
weekNumList.add(2);
weekNumList.add(3);
weekNumList.add(4);
weekNumList.add(5);
weekNumList.add(6);
weekNumList.add(7);
List<String> monthStrList = new ArrayList<>();
monthStrList.add("一月");
monthStrList.add("二月");
monthStrList.add("三月");
monthStrList.add("四月");
monthStrList.add("五月");
monthStrList.add("六月");
monthStrList.add("七月");
monthStrList.add("八月");
monthStrList.add("九月");
Flux<String> weekStrFlux = Flux.fromIterable(weekStrList);
Flux<String> monthStrFlux = Flux.fromIterable(monthStrList);
weekStrFlux.mergeWith(monthStrFlux).subscribe(System.out::print);
// 周一周二周三周四周五周六周日一月二月三月四月五月六月七月八月九月
看结果使用mergeWith方法是控制的合并的Flux的顺序,在调用mergeWitn方法的那块,如果将weekStrFlux和monthStrFlux调换下,就会发现打印的是 (一月二月。。。。周一周二)。
如果是想两个Flux之间交叉着合并,可以使用zip方法:
Flux<String> weekFlux = Flux.fromIterable(weekStrList);
Flux<String> monthFlux = Flux.fromIterable(monthStrList);
Flux.zip(weekFlux, monthFlux).subscribe(x -> {
System.out.println("第一个是:"+x.getT1()+" 第二个是:"+x.getT2());
});
第一个是:周一 第二个是:一月
第一个是:周二 第二个是:二月
第一个是:周三 第二个是:三月
第一个是:周四 第二个是:四月
第一个是:周五 第二个是:五月
第一个是:周六 第二个是:六月
第一个是:周日 第二个是:七月
看打印结果可以看出来合并结果的数量是按最少的来。
first方法也是一个组合操作,它可以在多个Flux之间选择优先发布值的Flux中取值作为新的元素,存在多个合并的源Flux的发布数据的速度有快有慢的情况下就可以用first方法。
还有skip方法,以指定跳过的条目数和时间。take方法和skip方法有点相反,skip是跳过前几个,而take方法是只取前几个,也可以指定条目数量或者前多少秒发布的数据,也有filter方法,和stream流方法中一样,传入一个Predicate。
转换操作
Flux.fromIterable(weekStrList).map(x -> {
return "转换操作"+x;
}).subscribe(x -> {
System.out.println(x);
});
转换操作周一
转换操作周二
转换操作周三
转换操作周四
转换操作周五
转换操作周六
转换操作周日
Flux.fromIterable(weekStrList).flatMap(x -> {
return Mono.just(x).map(y -> {
return y+"flatMap";
});
}).subscribe(x -> {
System.out.println(x);
});
周一flatMap
周二flatMap
周三flatMap
周四flatMap
周五flatMap
周六flatMap
周日flatMap
转换操作的方法有map,flatMap,将已发布的数据项转换为其他形式的数据类型,两者不同的是,map操作是同步执行的,flatMap操作是异步的,flatMap并不像map操作那样简单的将一个对象转换到另一个对象,而是将每一个对象转换为一个新得Mono或者Flux,形成得Mono或者Flux会扁平为新得Flux,
逻辑操作
逻辑操作有any和all方法。
Flux.fromIterable(weekNumList).any(x -> {
return x > 5;
}
).doOnNext(x -> {
System.out.println(x.booleanValue());
}).subscribe();
true
Flux.fromIterable(weekNumList).all(x -> {
return x > 5;
}
).doOnNext(x -> {
System.out.println(x.booleanValue());
}).subscribe();
false
结束
上述那些方法都可以和subscribeOn方法结合使用时,flatMap操作可以释放Reactor反应式的异步能力,subscribeOn方法是用来描述如何并发的处理订阅,Scheduler支持的并发模型:
Parallel Scheduler:
并行调度器通常用于执行可以并行处理的任务,它可能使用多个线程来同时执行多个任务。
在Reactor中,可以使用Schedulers.parallel()来获取一个并行调度器实例。
并行调度器通常用于需要高吞吐量的场景,例如批量处理或数据转换。
Elastic Scheduler:弹性调度器会根据需要动态地创建和销毁线程,以应对工作负载的变化。
使用Schedulers.elastic()可以获取一个弹性调度器实例。
弹性调度器适用于处理大量短生命周期的异步任务,它可以根据需要扩展或缩小线程池的大小。
Single Scheduler:单线程调度器确保任务按照它们被提交的顺序依次执行。
使用Schedulers.single()可以获取一个单线程调度器实例。
单线程调度器适用于需要保证任务执行顺序的场景,例如按顺序处理事件或日志。
Immediate Scheduler:立即调度器在提交任务的同一个线程中立即执行任务,不进行任何异步处理。
这通常用于不需要异步执行的任务,或者作为其他调度器的回退选项。
Custom Scheduler:除了上述内置的调度器之外,你还可以创建自定义的Scheduler实现,以满足特定的并发模型或性能需求。
自定义调度器可以基于Java的ExecutorService或其他并发工具构建,并集成到Reactor的响应式流中。