原文链接
前言
在前面两章,我们分别介绍了反应式框架的基本原理以及常用情景下反应式框架的具体相关代码和测试,本篇我们会介绍一些进阶使用
组合运算
如果我们经常用到某些操作组合,则需要将这些操作和其他业务进行分离,提高代码复用性,除了直接抽出方法外,我们还可以通过指定手写方法变量,通过transform
或者transformDeferred
进行注入
@Service
public class DemoServiceImpl implements IDemoService {
public void testDemo() {
Function<Flux<Integer>, Flux<Integer>> oddAndInversion =
flux -> flux.filter(num -> 1 == num % 2).map(num -> -num);
Flux.just(0, 1, 2, 3, 4, 5)
.transform(oddAndInversion)
.doOnNext(element -> System.out.printf(element + " "))
.subscribe();
//-1 -3 -5
}
}
如果我们希望对于不同的订阅有不同的处理,我们可能会考虑在方法变量中添加判断语句,比如:
public void testDemo() {
var cnt = new AtomicInteger(0);
Function<Flux<Integer>, Flux<Integer>> oddAndInversion =
flux -> {
if (1 == cnt.incrementAndGet()) {
return flux.filter(num -> 1 == num % 2).map(
num -> -num);
} else {
return flux.filter(num -> 0 == num % 2).map(num -> -num);
}
};
var numFlux = Flux.just(0, 1, 2, 3, 4, 5)
.transform(oddAndInversion)
.doOnNext(element -> System.out.printf(element + " "));
numFlux.subscribe(num -> System.out.println("Subscriber 1 "));
numFlux.subscribe(num -> System.out.println("Subscriber 2 "));
}
此时我们会发现输出为
-1 Subscriber 1
-3 Subscriber 1
-5 Subscriber 1
-1 Subscriber 2
-3 Subscriber 2
-5 Subscriber 2
和预期不符,这是由于在订阅时候,通道已经建立,两个订阅都依赖同一个transform
,而这个transform
的结果已经在订阅前产生了,也就是flux.filter(num -> 1 == num % 2).map(num -> -num);
,所以我们这里需要延迟transform
的计算时间,改成调用transformDeferred
就可以活得期望的输出:
-1 Subscriber 1
-3 Subscriber 1
-5 Subscriber 1
0 Subscriber 2
-2 Subscriber 2
-4 Subscriber 2
冷热发布
对于冷发布来说,会为每一个订阅生成数据,在没有订阅的情况下,则不会生成生成数据
对于热发布来说,并不依赖订阅者,没有订阅者不影响发布流程的继续,故而在这种发布模式下,后进入的订阅者是无法观测到之前的数据的,只能观测到订阅之后发布的数据
此前给的绝大多数代码都是冷发布,这里给出热发布的代码示例:
public void testDemo() {
//Build
Sinks.Many<Integer> hotSource = Sinks.unsafe().many().multicast().directBestEffort();
Flux<Integer> hotFlux = hotSource.asFlux();
//Publish no sub
hotSource.emitNext(1, FAIL_FAST);
hotSource.emitNext(2, FAIL_FAST);
//Subscriber 1
hotFlux.subscribe(element -> System.out.println("Subscriber 1 : " + element));
//Publish 1 sub
hotSource.emitNext(3, FAIL_FAST);
hotSource.tryEmitNext(4).orThrow();
//Subscriber 2
hotFlux.subscribe(element -> System.out.println("Subscriber 2 : " + element));
//Publish 2 sub
hotSource.emitNext(5, FAIL_FAST);
hotSource.emitNext(6, FAIL_FAST);
//Complete
hotSource.emitComplete(FAIL_FAST);
}
此时我们可以得到输出为:
Subscriber 1 : 3
Subscriber 1 : 4
Subscriber 1 : 5
Subscriber 2 : 5
Subscriber 1 : 6
Subscriber 2 : 6
可以观察到,最开始的两个数据并没有被处理,而3和4只被第一个订阅处理了,而5和6则被两个订阅都处理了,和期望一致。当然,我们还可以做一些限定,比如tryEmitNext
和orThrow
,如果没有推成功则直接异常,或者使用isFailure
和isSuccess
做自定义的逻辑处理
合并广播
正常情况下,我们在订阅之后应该就是可以获取数据的状态了,无论是冷发布还是热发布。但是如果我们希望先订阅,然后再进行其他一切操作(比如整合其他订阅流程,最后再一起触发数据获取),那么我们就需要使用ConnectableFlux
,我们将上方冷热发布的演示稍作修改:
public void testDemo() {
//Build
Sinks.Many<Integer> hotSource = Sinks.unsafe().many().multicast().directBestEffort();
Flux<Integer> hotFlux = hotSource.asFlux();
ConnectableFlux<Integer> connectableFlux = hotFlux.publish();
//Publish no sub
hotSource.emitNext(1, FAIL_FAST);
hotSource.emitNext(2, FAIL_FAST);
//Subscriber 1
connectableFlux.subscribe(element -> System.out.println("Subscriber 1 : " + element));
//Publish 1 sub
hotSource.emitNext(3, FAIL_FAST);
hotSource.emitNext(4, FAIL_FAST);
//Subscriber 2
connectableFlux.subscribe(element -> System.out.println("Subscriber 2 : " + element));
connectableFlux.connect();
//Publish 2 sub
hotSource.emitNext(5, FAIL_FAST);
hotSource.emitNext(6, FAIL_FAST);
//Complete
hotSource.emitComplete(FAIL_FAST);
}
获得输出为:
Subscriber 1 : 5
Subscriber 2 : 5
Subscriber 1 : 6
Subscriber 2 : 6
此时我们就可以观察到ConnectableFlux
合并广播的效果,在connectableFlux.connect();
执行之后,才实际将两个订阅者订阅发布,当然我们也可以自定义触发逻辑,比如设置两个订阅者订阅时候就自动触发connect
public void testDemo() {
Sinks.Many<Integer> hotSource = Sinks.unsafe().many().multicast().directBestEffort();
Flux<Integer> hotFlux = hotSource.asFlux().publish().autoConnect(2);
hotSource.emitNext(1, FAIL_FAST);
hotFlux.subscribe(element -> System.out.println("Subscriber 1 : " + element));
hotSource.emitNext(3, FAIL_FAST);
hotFlux.subscribe(element -> System.out.println("Subscriber 2 : " + element));
hotSource.emitNext(5, FAIL_FAST);
hotFlux.subscribe(element -> System.out.println("Subscriber 3 : " + element));
hotSource.emitNext(7, FAIL_FAST);
hotSource.emitComplete(FAIL_FAST);
}
此时观测到结果:
Subscriber 1 : 5
Subscriber 2 : 5
Subscriber 1 : 7
Subscriber 2 : 7
Subscriber 3 : 7
在订阅数量达到两个时候触发connect
此时5被两个订阅处理,而后续发布的7被三个订阅处理,和预期一致
批处理
批处理就是将序列中的元素重新聚合,分为多个聚合方便后续业务处理,一般有三种操作方式:Grouping
,Windowing
以及Buffering
Grouping
这里的分组的操作和正常流分组操作差不多,通过groupBy
将序列元素由Flux<T>
分成Flux<GroupedFlux<T>>
,示例代码如下:
public void testDemo() {
Flux.range(-5, 10)
.groupBy(num -> num > 0 ? "pos" : 0 == num ? "z" : "neg")
.doOnNext(nt -> System.out.println())
.subscribe(
outSub -> outSub.doOnSubscribe(sub -> System.out.println(outSub.key()))
.subscribe(inSub -> System.out.print(inSub + " "))
);
}
输出为:
neg
-5 -4 -3 -2 -1
z
0
pos
1 2 3 4
Windowing
窗口可以根据索引划分,也可以根据谓词划分,所以窗口相交于分组,是有可能出现空或者元素的重合的情况,通过window
或其他衍生运算符将Flux<T>
划分为Flux<Flux<T>>
,示例代码如下:
public void testDemo() {
Flux.range(1, 5)
.window(2)
.doOnNext(nt -> System.out.println())
.subscribe(
outSub -> outSub.doOnSubscribe(sub -> System.out.println("Some sub: "))
.subscribe(inSub -> System.out.print(inSub + " "))
);
}
Some sub:
1 2
Some sub:
3 4
Some sub:
5
public void testDemo() {
Flux.range(1, 5)
.windowWhile(i -> i >3)
.doOnNext(nt -> System.out.println())
.subscribe(
outSub -> outSub.doOnSubscribe(sub -> System.out.println("Some sub: "))
.subscribe(inSub -> System.out.print(inSub + " "))
);
}
Some sub:
Some sub:
Some sub:
Some sub:
4 5
Buffering
缓冲的用法和窗口基本一直,只是缓冲将Flux<T>
聚合成了Flux<List<T>>
,示例代码如下:
public void testDemo() {
Flux.range(1, 5)
.buffer(2)
.doOnNext(nt -> System.out.println())
.subscribe(System.out::println);
}
[1, 2]
[3, 4]
[5]
并行
我们在第一篇提到,反应式相比于传统命令式框架能够更好的处理高并发场景,也就是使用更少的线程资源处理更多请求。那么如果我们确实需要并行处理任务的时候,示例代码如下:
public void testDemo() {
Flux.range(1, 4)
.doOnSubscribe(sub -> System.out.println(Thread.currentThread().getName()))
.parallel(2)
.runOn(Schedulers.parallel())
.map(num -> {
var doubleNum = 2 * num;
System.out.println(Thread.currentThread().getName() + " " + doubleNum);
return 2 * doubleNum;
})
.sequential()
.map(num -> {
var doubleNum = 2 * num;
System.out.println(Thread.currentThread().getName() + " " + doubleNum);
return 2 * doubleNum;
})
.subscribe();
}
输出为:
reactor-http-nio-2
parallel-3 4
parallel-2 2
parallel-3 16
parallel-2 6
parallel-3 8
parallel-3 24
parallel-3 8
parallel-3 32
我们可以使用.parallel(n).runOn(Schedulers.parallel())
完成并行操作,单纯使用parallel()
并不会将后续任务处理并行,需要将运行轨道传入序列才能真正实现并行,一般使用Schedulers.parallel()
,当并行任务执行完成,使用sequential()
合并轨道
上下文
在上一篇我们有提到,
一般命令式编程的结构是将请求绑定在线程上,那么该请求的生命周期中需要用到数据我们就可以
ThreadLocal
来处理,因为这里请求生命周期中留存数据和线程的上下文是重合的。但是对于反应式来说,一个线程是很可能会服务多个请求的,此时就没办法使用ThreadLocal
来处理了
在反应式框架当中,我们一般使用Context
代替原有ThreadLocal
执行的功能,基本使用如下:
public void testDemo() {
String key = "key";
Flux.just(1, 2)
.flatMap(num -> Flux.deferContextual(ctx ->
Flux.just(num + Integer.parseInt(ctx.get(key).toString()))
))
.contextWrite(ctx -> ctx.put(key, 100))
.subscribe(System.out::println);
}
打印结果:
101
102
当然,我们这里也可以直接在订阅时加入上下文,去掉.contextWrite(ctx -> ctx.put(key, 100))
,使用.subscribe(System.out::println, null, null, Context.of("key", 100))
代替.subscribe(System.out::println);
效果是相同的