反应式编程入门及原理

一、前言

在spring cloud gateway中大量使用了spring5才引入的反应式编程(Reactive Programming)。在此背景下,本文介绍下反应式编程相关使用及原理。

二、反应式编程简介

    反应式编程最早是由微软提出并引入到 .NET 平台中,随后 ES6 也引入了类似的技术。在 Java 平台上,较早采用反应式编程技术的是 Netflix 公司开源的 RxJava 框架。现在大家比较熟知的 Hystrix 就是以 RxJava 为基础开发的。而spring5又借鉴了RxJava。     反应式编程主要优点有:         1、整体采用了观察者模式,异步解耦,提高服务器的吞吐量。         2、内部提出了背压(Backpressure)概念,可以控制消费的速度         3、书写方式与迭代器,stream类似,方便使用者理解。

2.1 与迭代器对比

Iterable Observable
迭代 next() onNext()
异常 throws Exception onError()
完成 !hasNext() onCompleted()

    上面表格中的 Observable 那一列表示了观察者接受到相关事件时触发的动作。如果将迭代器看作是拉模式,那观测者模式便是推模式。被观察者(Subject)主动的推送数据给订阅者(Subscriber),触发 onNext 方法,出现异常时触发onError(),完成后触onCompleted()。

2.2 与stream对比

事件 stream Observable
映射 map() map()
过滤 filter() filter()

    与stream对比可以看出,Reactive Programming也是通过类似的数据流方式来处理订阅的数据。不同点在于stream无法控制消息发送速度,而反应式编程中如果 Publisher 发布消息太快,超过了 Subscriber 的处理速度,反应式编程提供了背压机制来控制 Publisher的速度。

三、Reactor 入门

3.1 Reactor中主要的类

    Mono 实现了 org.reactivestreams.Publisher 接口,代表0到1个元素的发布者。     Flux 同样实现了 org.reactivestreams.Publisher 接口,代表0到N个元素的发表者。     Subscriber观察者,用来观察Publisher相关动作。     Subscription解耦Subscriber和Publisher。     Mono和Flux可以相互转换。多个Mono可以合并成一个Fulx,一个Flux也可以转化成Mono。

3.2 一个栗子

    假设有个名单列表,要根据名单获取对应名字的邮箱,并且过滤掉邮箱长度小于10的邮箱,最后再将符合条件的邮箱打印出来。 使用stream编程如下所示。

List<String> names = Collections.arrayToList(new String[]{"tom", "Bob", "zhangsan", "lisi"});
System.out.println(
	names.stream()
	.map(s -> s.concat("@qq.com"))
	.filter(s -> s.length() > 10)
	.toArray()
);
复制代码

使用Reactive编程如下所示。

Flux.just("Tom", "Bob", "zhangsan", "lisi")
	.map(s -> s.concat("@qq.com"))
	.filter(s -> s.length() > 10)
	.subscribe(System.out::println);
复制代码

    通过上述例子可以看出,stream和Reactive在形式上有相似之处,都是先创建数据源,然后经过中间过程处理转换,最后再消费中间处理结果。

Flux.just("Tom", "Bob", "zhangsan", "lisi")
复制代码

    Flux.just()创建一个Flux的发布者。除了使用just方法外,还有fromCallable,fromIterable等其他方式用来从不同场景中创建publisher。

map(s -> s.concat("@qq.com"))
复制代码

    map的含义就是映射,在上一步中创建了一个4个元素序列的发布者,在该步骤中将每个序列元素进行转换,在每个名称后面加上邮箱后缀。

filter(s -> s.length() > 10)
复制代码

    过滤步骤,将经过映射的4个元素进行过滤,剔除掉长度不大于10的。中间过程书写形式和含义与stream类似。

subscribe(System.out::println);
复制代码

    该步骤是最终的订阅阶段,之前创建的都是被观察者,该步骤是创建一个观察者subscriber。其中subscriber的具体行为就是System.out::println打印出之前处理过的元素。至此一个订阅发布的过程就结束了。

四、Reactor的工作原理

    在之前的章节中已经说过,反应式编程的核心就是一个观察者模式。 观察者模式 Flux和Mono相当于观察者模式中的subject,当Flux或Mono调用subscribe方法时,相当于subject发出了一个Event,从而让订阅此事件的观察者进行消费。那Flux框架具体如何实现这套机制呢,还是以上节中的例子跟踪下它是如何工作的。

Flux.just("Tom", "Bob", "zhangsan", "lisi")
	.map(s -> s.concat("@qq.com"))
	.filter(s -> s.length() > 10)
	.subscribe(System.out::println);
复制代码

本文基于3.1.9.RELEASE版本。

4.1 申明阶段

4.1.1 Flux.just()

    进入just方法,经过若干跳转后,进入如下方法。

public static <T> Flux<T> fromArray(T[] array) {
	if (array.length == 0) {
		return empty();
	}
	if (array.length == 1) {
		return just(array[0]);
	}
	return onAssembly(new FluxArray<>(array));
}
复制代码

    onAssembly是一个钩子方法,暂时忽略。最终就是new FluxArray<>(array)一个对象创建出了一个FluxArray。点击FluxArray的构造函数中,可以看看到,只是把array赋值给了对象内部的array。

final T[] array;
@SafeVarargs
public FluxArray(T... array) {
	this.array = Objects.requireNonNull(array, "array");
}
复制代码

4.1.2 map

    Flux.just方法只是创建了一个FluxArray对象,回到最开始定义的地方,下一步执行的是map方法。定义如下所示。

public final <V> Flux<V> map(Function<? super T, ? extends V> mapper) {
	if (this instanceof Fuseable) {
		return onAssembly(new FluxMapFuseable<>(this, mapper));
	}
	return onAssembly(new FluxMap<>(this, mapper));
}
复制代码

    上一步创建的FluxArray是一个Fuseable,所以执行if条件里的逻辑,创建一个FluxMapFuseable对象,FluxMapFuseable的构造函数中有两个参数,this和mapper。this就是上一步创建出来的FluxArray,mapper就是我们自定义的Lambda表达式,即:s -> s.concat("@qq.com")。再点击进入FluxMapFuseable的构造函数中。

FluxMapFuseable(Flux<? extends T> source,
			Function<? super T, ? extends R> mapper) {
	super(source);
	this.mapper = Objects.requireNonNull(mapper, "mapper");
}
复制代码

    从这个构造函数可以看出,source是上一步骤just得到的FluxArray,mapper是对应map的Lambda表达式,所以当执行map操作的时候,其实是又将FluxArray进行封装,得到了一个新的FluxMapFuseable对象。

4.1.3 filter

    再次回到开始的申明地方,在执行完map操作后,接着执行filter。同理,点击filter方法,可以看到如下代码。

public final Flux<T> filter(Predicate<? super T> p) {
	if (this instanceof Fuseable) {
		return onAssembly(new FluxFilterFuseable<>(this, p));
	}
	return onAssembly(new FluxFilter<>(this, p));
}
复制代码

    在看过map的操作后,这一步骤其实就相当熟悉了,filter步骤将上一步map操作得到的FluxMapFuseable方法又一次封装成了FluxFilterFuseable对象。

4.1.4 申明总结

    从上面的定义可以看出,申明阶段就是一层一层的创建各种Flux对象,并没有实际执行任何操作。通过just,map,filter等操作,将发布者一层一层的封装,从最开始的FluxArray对象,到FluxMapFuseable对象以及最后的FluxFilterFuseable对象。如下图所示。 null

4.2 订阅阶段

4.2.1 subscribe、onsubscribe

    上述例子中,just,map,filter只是创建了一个个的对象。并没有实际执行相关逻辑。当调用被观察者的subscribe方法时,会为被观察者添加相应的观察者,同时触发观察者相关方法,从而使整个观察者模式得以进行下去。接着看下Fulx的subscribe方法。     经过一系类的jump后,最终会调用Flux的subscribe,如下所示。

public abstract void subscribe(CoreSubscriber<? super T> actual);
复制代码

    该方法是一个抽象方法,需要看下子类是如何实现的。还记得上一步骤中filter后产生的对象嘛?FluxFilterFuseable是Flux的一个具体实现,当调用subscribe后,会跳转到FluxFilterFuseable的subscribe方法,代码如下。

public void subscribe(CoreSubscriber<? super T> actual) {
	if (actual instanceof ConditionalSubscriber) {
		source.subscribe(new FilterFuseableConditionalSubscriber<>((ConditionalSubscriber<? super T>) actual,
				predicate)); // 1
		return;
	}
	source.subscribe(new FilterFuseableSubscriber<>(actual, predicate)); // 2
}
复制代码

    传进来的actual是System.out::println,也就是我们最终执行的表达式,它被封装成了一个LambdaSubscriber观察者,predicate为filter指定的表达式s -> s.length() > 10,source为上一步骤中生成的FluxMapFuseable对象。根据对象情况,代码会走到2处。2处的逻辑就是将actual和predicate封装成一个订阅者去订阅source也就是FluxMapFuseable对象。     接着代码会去调用source的subscribe方法,也就是FluxMapFuseable对应的subscribe方法。

public void subscribe(CoreSubscriber<? super R> actual) {
	if (actual instanceof ConditionalSubscriber) { //1
		ConditionalSubscriber<? super R> cs = (ConditionalSubscriber<? super R>) actual;
		source.subscribe(new MapFuseableConditionalSubscriber<>(cs, mapper));
		return;
	}
	source.subscribe(new MapFuseableSubscriber<>(actual, mapper)); //2
}
复制代码

    代码还是会走到2出,这里传入的actual是上一步骤中封装了System.out::println和s -> s.length() > 10的观察者,mapper为s -> s.concat("@qq.com"),从这段代码可以看出,所做的逻辑就是将上一步中的观察者和mapper又封装成了新的观察者。一层一层的套娃。     最后,看下本步骤中的source,也就是FluxArray对象的subscribe方法。

public static <T> void subscribe(CoreSubscriber<? super T> s, T[] array) {
	if (array.length == 0) {
		Operators.complete(s);
		return;
	}
	if (s instanceof ConditionalSubscriber) { // 1
		s.onSubscribe(new ArrayConditionalSubscription<>((ConditionalSubscriber<? super T>) s, array));
	}
	else {
		s.onSubscribe(new ArraySubscription<>(s, array)); // 2
	}
}
复制代码

    FluxArray是数据的源头,传入的array为我们定义的"tom", "Bob", "zhangsan", "lisi"名字。s为上一步骤中创建的subscriber。在数据的源头可以看出作为观察者模式的触发项,该步骤中触发了观察者的onsubscribe方法。同时为了解耦观察者和被观察者,创建一个ArraySubscription对象。FluxArray的subscribe会执行2处代码,s.onSubscribe(new ArraySubscription<>(s, array)),这里的s是上一步骤中创建的MapFuseableSubscriber中的onSubscribe方法,对应代码如下所示。

@Override
public void onSubscribe(Subscription s) {
    if (Operators.validate(this.s, s)) {
		this.s = (QueueSubscription<T>) s;
		actual.onSubscribe(this);
	}
}
复制代码

    actual是FilterFuseableSubscriber对象,本质就是赋值后,然后调用FilterFuseableSubscriber的onSubscribe方法。FilterFuseableSubscriber对应的onSubscribe方法如下所示。

@Override
public void onSubscribe(Subscription s) {
	if (Operators.validate(this.s, s)) {
		this.s = (QueueSubscription<T>) s;
		actual.onSubscribe(this);
	}
}
复制代码

    和MapFuseableSubscriber类似。actual对应的是LambdaSubscriber,也就是System.out::println。LambdaSubscriber的onsubscribe如下所示。

public final void onSubscribe(Subscription s) {
	if (Operators.validate(subscription, s)) {
		this.subscription = s;
		if (subscriptionConsumer != null) {
			try {
				subscriptionConsumer.accept(s); // 1 
			}
			catch (Throwable t) {
				Exceptions.throwIfFatal(t);
				s.cancel();
				onError(t);
			}
		}
		else {
			s.request(Long.MAX_VALUE); // 2
		}
	}
}
复制代码

    1和2代码的最终逻辑都一样,都会执行request方法。背压的原理就是通过这个request来实现的,观察者可以通过request来指定一次性订阅多少数据。     总结一下:一个subscribe方法其实是创建了三个观察者,与创建发布者类似,创建的观察者也是一层一层嵌套。从最外层的subscriber与上一层的操作结合生成一个新的subscriber。再继续向上调用,最终调用到数据源头。然后从数据源头开始一层一层再出发观察者的onsubscribe。 null

4.2.2 request

public final void onSubscribe(Subscription s) {
	if (Operators.validate(subscription, s)) {
		this.subscription = s;
		if (subscriptionConsumer != null) {
			try {
				subscriptionConsumer.accept(s); // 1 
			}
			catch (Throwable t) {
				Exceptions.throwIfFatal(t);
				s.cancel();
				onError(t);
			}
		}
		else {
			s.request(Long.MAX_VALUE); // 2
		}
	}
}
复制代码

    在onsubscribe阶段最终会调用s的request方法。还记得s嘛?s是在解耦观察者和被观察这创建出来的subscription。

public static <T> void subscribe(CoreSubscriber<? super T> s, T[] array) {
	if (array.length == 0) {
		Operators.complete(s);
		return;
	}
	if (s instanceof ConditionalSubscriber) { // 1
		s.onSubscribe(new ArrayConditionalSubscription<>((ConditionalSubscriber<? super T>) s, array));
	}
	else {
		s.onSubscribe(new ArraySubscription<>(s, array)); // 2
	}
}
复制代码

    就是这里的ArraySubscription对象。看下这个对象的request方法。

@Override
public void request(long n) {
	if (Operators.validate(n)) {
		if (Operators.addCap(REQUESTED, this, n) == 0) {
			if (n == Long.MAX_VALUE) {
				fastPath(); // 1
			}
			else {
				slowPath(n); // 2
			}
		}
	}
}

void fastPath() {
	final T[] a = array;
	final int len = a.length;
	final Subscriber<? super T> s = actual;

	for (int i = index; i != len; i++) {
		if (cancelled) {
			return;
		}

		T t = a[i];

		if (t == null) {
			s.onError(new NullPointerException("The " + i + "th array element was null"));
			return;
		}

		s.onNext(t);
	}
	if (cancelled) {
		return;
	}
	s.onComplete();
}
复制代码

    直接看下fastPath(),代码都贴在了一起。到这里就真正开始消费。通过一个for循环,调用Subscriber的onNext方法,onNext方法执行完毕后,执行Subscriber的onComplete方法。这里的s是MapFuseableConditionalSubscriber,看下它的onNext方法。

public void onNext(T t) {
	if (sourceMode == ASYNC) {
		actual.onNext(null);
	}
	else {
		if (done) {
			Operators.onNextDropped(t, actual.currentContext());
			return;
		}
		R v;

		try {
			v = Objects.requireNonNull(mapper.apply(t),
					"The mapper returned a null value."); // 1
		}
		catch (Throwable e) {
			onError(Operators.onOperatorError(s, e, t, actual.currentContext()));
			return;
		}

		actual.onNext(v); // 2
	}
}
复制代码

    在1处执行mapper对应的Lambda表达式,在2处执行下一步的Subscriber的onNext方法。下一步是Filter,再下一步是最终的System.out::println。最后onNext都执行换成后,执行s的onComplete方法,道理也是一样的,都是从最开始Subscriber的onComplete方法一层一层执行。至此一个完成的观察者模式的执行情况就完成了。

4.3 总结

1、申明阶段只是创建了一个个的被观察者,把动作包装成对象,其他什么事都没做,直到调用被观察者的subscribe方法,为被观察者添加观察者。 2、添加观察者后,每一个申明步骤都会创建一个新的观察者,观察上一步骤的被观察者,直到最外层被观察者触发onSubscribe,接着按照刚才添加的观察者一层层调用对应的onSubscribe方法,最后触发request方法。 3、当触发到最外层的request后,就执行真正的逻辑,再一层层调用观察者的onNext方法。最后完成后调用onComplete方法。

Q1:反应式编程的背压怎么实现的? A1:观察者通过request中的传参,控制消费速度,从而实现反应式编程的背压特性,在声明Subscriber时,我们可以重写Subscriber接口,实现里面的request方法,来时控制订阅的速度。

Q2:为什么反应式编程可以提高吞吐量? A2:从刚才的实现逻辑可以看出,被观察者只是申明了数据操作的定义,实际上什么都没做,几乎不消耗cpu,io等资源。在使用反应式编程处理web请求时,一般会写成如下形式。

@RequestMapping("/mail")
public Flux<String> getUserMail() {
  return Flux.just("Tom", "Bob", "zhangsan", "lisi")
	.map(s -> s.concat("@qq.com"))
	.filter(s -> s.length() > 10)
}
复制代码

只是定义了被观察者,能够很快完成一个请求的处理。 因此在实际应用中,如http请求,短时间如果有大量请求到来,可以快速创建相关请求,增大接口的吞吐量,但是接口的处理速度并不会加快,因为需要处理的请求耗时,都不会减小。

Q3:代码中如何控制订阅的速度? A3:如果直接使用subecribe(System.out::println),默认走的request(Long.MAX_VALUE)。如果要控制速度有如下几种方式。 1、自己实现Subscriber,上述例子中使用了subecribe(System.out::println),默认创建了一个LambdaSubscriber观察者,在该观察者的onSubscribe方法中执行了s.request(Long.MAX_VALUE);方法,所以可以自己实现一个Subscriber自定义策略给request中传入不同的值,从而控制速度。比如发现当前流量大于500时,限制速度。

public final void onSubscribe(Subscription s) {
		if (rate > 500) {
		  s.request(500)
		} else {
		  s.request(Long.MAX_VALUE);
		}
}
复制代码

2、通过flux的limitrate方式实现调整request数量

Flux.range(1,10)
    .log()
    .limitRate(2)
    .subscribe();
复制代码

3、实现现有BaseSubscriber类,重写里面的onSubscribe方法,本质跟1类似。

猜你喜欢

转载自juejin.im/post/7034350525197860878
今日推荐