Reactive声明式响应式编程到底带给我们什么?

前言

希望有reactive库的使用经验的小伙伴阅读,会比较有体会

记得我第一次接触Reactive是在读一本知秋大佬写的RxJava书的时候,书名叫《Java编程方法论:响应式RxJava与代码设计实战》,这本书让我初识了Reactive的底层实现范式,如果有读过RxJava的源码就可以知道,其底层类似扩展了面向对象的设计模式 — 观察者模式,一个事件数据流从生产者发射,一个个消费者(观察者)将会(观察到并)响应从上游发射的数据流,并且RxJava提供了大量的操作符可以很灵活方便的管理数据流的变化,称之为响应变化即为响应式编程,当时对Reactive仅仅停留在知道他的技术实现的层面,但并不知道使用Reactive的优势,或者说Reactive解决了什么场景下的问题(为什么使用Reactive?)。

我曾大量搜索网上博客讨论Reactive的优势、好处,我感觉大部分文章都没讲到精华,这篇文章中我将对Reactive的理解写下,希望能让更多人理解为什么要使用Reactive,这也是本篇文章的诞生初衷之一。

本篇文章对于Reactive的很多思考都来自于 小马哥的一篇对于 Reactive Programming 的理解,和 Reactor库官方文档 在这里感谢这些作者。

我认为有以下场景驱使我们需要使用Reactive:

  • 我们需要异步编程去更有效率的利用系统资源
  • 处理事件流例如NIO可读可写事件、鼠标点击、键盘输入等等,对事件作出一些响应

接下来逐个探讨这些议题。

1. 异步编程,效率利用系统资源

1.1 阻塞可能是浪费的

引用自Reactor库文档中的 3.1节Blocking Can Be Wasteful:

Usually, Java developers write programs by using blocking code. This practice is fine until there is a performance bottleneck. Then it is time to introduce additional threads, running similar blocking code. But this scaling in resource utilization can quickly introduce contention and concurrency problems.

Worse still, blocking wastes resources. If you look closely, as soon as a program involves some latency (notably I/O, such as a database request or a network call), resources are wasted because threads (possibly many threads) now sit idle, waiting for data.

So the parallelization approach is not a silver bullet. It is necessary to access the full power of the hardware, but it is also complex to reason about and susceptible to resource wasting.

这里Reactor的大概意思是:

  • 当程序中有一些潜在的IO例如数据库请求、网络调用,线程资源由于这段时间需要等待IO数据的到来而被闲置,此时如果请求数量增大,就会遇到一个性能瓶颈(performance bottleneck),为了及时响应处理别的请求,此时往往需要引入额外的线程,但随之而来也会引入一些资源竞争和并发问题,Reactor认为,这里因为阻塞而引入额外的这部分线程是被浪费掉的

这里提到同步阻塞的这部分线程是被浪费掉的,是因为他什么事都没做,却要停在那边,占用了一些内存资源(线程上下文堆栈保存、创建销毁线程开销)

来看一个例子,假设A用户登陆系统的同时,B用户也同时进来登陆:
在这里插入图片描述

通常,我们引入多线程即可解决用户B的延时响应问题:
在这里插入图片描述

在这个互联网火热的时代,万级的并发都是常态,那么如果一万个用户同时进来登陆系统,为了保证响应性及时,是否开一万个线程呢?肯定不可能,需要的系统资源将是无止境的,这对硬件要求过高了。那如果我们只使用100个线程循环使用呢(线程池)?那么同一时间有一万用户同时登陆,在前0.8s只会有100个用户能及时得到响应,后面来的9900用户都会阻塞0.8s然后继续有100个用户能得到响应,以此类推大部分的用户将很久得不到响应。

这里的关键点就在于idleTime太久了,让线程白白在那边干等着,造成只能引入额外线程才能及时处理别的用户的请求

其实Reactor暗示了,异步非阻塞将避免浪费这部分线程,那么异步非阻塞是如何做到的呢?
在这里插入图片描述

关键点在于非阻塞的请求调用数据到来时的通知,数据到来期间线程是不管的,可以继续处理别的请求,这样对比来看,阻塞确实可能是浪费资源的

虽然每个用户都增长的少许延迟时间(实际场景的耗时和例子其实差别挺大,这里只不过放大数据,让大家能更形象看出非阻塞模型的优势之处),但是在阻塞模型中我们本来需要使用3个线程(3个用户并发)保证延时及时性,使用了非阻塞模型只需要一个线程即可做到,底层技术实现来说,以Netty框架的NIO非阻塞模型为例,就与多路复用有关系,大致来说是注册读写事件,潜在的IO操作交给了内核去处理,用户态的线程就不需要等待,如果是epoll模型,IO结果就会由内核来通知用户态线程,等待IO期间,用户态线程自由分配动作处理其他请求。

我们结合Spring5官方文档的几句话一起来理解一下:

Reactive and non-blocking generally do not make applications run faster. They can, in some cases, for example if using the WebClient to execute remote calls in parallel. On the whole it requires more work to do things the non-blocking way and that can increase slightly the required processing time.

The key expected benefit of reactive and non-blocking is the ability to scale with a small, fixed number of threads and less memory. That makes applications more resilient under load because they scale in a more predictable way.

  • 非阻塞配合Reactive通常不能使得应用运行更快速,甚至还可能会增加一些处理时间
  • 关键点在于Reactive和非阻塞可以使得少而固定的线程和少量的内存消耗,就可以使得应用具有很好的伸缩性

关键在于,只需要少量的系统资源,就可以具有很好的伸缩性,这里大概指的是在并发量激增,连接数达到万级以上,应用始终能使用少量资源去很好的处理用户请求。

下面我举一个我在现实项目中遇到的问题,来说明非阻塞的优势。

我的项目中要求使用SpringCloud-Gateway作为微服务网关,这个网关框架使用的是WebFlux,完全使用了Reactor这个Reactive库来进行编程,说实话我踩了无数的坑,由于国内Reactive的不普及,导致这方面的资料相当少,解决SpringCloud-Gateway的问题的时候,往往要去SpringCloud的Github里的issue区寻找答案,在issue区里,SpringCloud的一位Committer曾经说过几句让我疑惑的话:

下面提到被废除的组件指的是在SpringCloud Hoxton版本中Reactive环境下被废除

  • LoadBalancerClientFilter被废除,因为它是阻塞的API,被非阻塞的ReactiveLoadBalancerClientFilter替代
    在这里插入图片描述

  • HttpMessageConverters在Reactive中不被使用,因为它是阻塞的API,被非阻塞的HttpMessageReader、HttpMessageWriter替代
    在这里插入图片描述

背景:HTTP请求包体由于都是raw的字节数组,我们需要根据Content-Type类型例如application/json,才知道要将字节数组转化为json字符串格式,这个工作就是由HttpMessageConverters来做的

这引发了我的思考,为什么他们是阻塞的就要被废除?

仔细思考了一下,其实这俩组件被Reactive替代的关键点其实在于他们都存在一个潜在的阻塞IO动作,LoadBalancerClientFilter中使用了LoadBalancerClient使用阻塞式Http调用去注册中心拿到服务地址,HttpMessageConverters使用阻塞式的流去读写Http的Body。

像Netty一样,SpringCloudGateway(后简称Gateway)也使用了固定少量的线程去处理大量连接,秘诀是非阻塞的NIO,如果线程被阻塞在IO操作上,必然会影响吞吐量,因为处理连接请求的线程一旦被阻塞,后面几个连接请求必然得不到处理,从而降低了吞吐量,这也就是为什么Netty的使用过程中不能阻塞worker线程的原因。

这引发了我进一步思考,在Gateway中我也使用到了Feign组件完成微服务之间的调用,其底层我是使用的OKHttp去完成HTTP调用,其也是阻塞式的,如果我将其更换成Netty形式的非阻塞IO,是否能增加吞吐量呢?这只能用测试来说明。我将Feign组件更换成Reactive形式,底层使用了Netty完成HTTP调用:

开3000个线程,每个线程分别循环10次和30次访问,查看吞吐量的提升

测试一个比较复杂的处理接口,需要调用两次Feign

  • 每个线程循环调用10次(阻塞式):500TPS

  • 每个线程循环调用10次(非阻塞式):300TPS

提升了66%的吞吐量


  • 每个线程循环调用30次(阻塞式):680TPS

  • 每个线程循环调用30次(非阻塞式):420TPS

提升了60%的吞吐量

不要吐槽TPS低,这里测试的接口被测试机硬件性能和路由调用其他微服务的接口TPS限制,这里仅仅关注吞吐量的提升幅度

原来一个潜在的阻塞IO操作会影响这么多吞吐量(仅针对Gateway系统下来说,因为在Gateway中,处理请求的线程数往往都很少,可能仅仅只是CPU核心*2的数量,这时候的阻塞IO操作真的会严重影响吞吐量,如果在传统编程下,像阻塞的Servlet,一般服务器都直接开200多个线程处理请求,一个线程处理一个连接的请求,这时候阻塞一个线程没什么,有其他很多储备的线程去处理其他连接的请求),这里的吞吐量提升,使用NIO的Netty框架功不可没,但这与Reactive有什么关系呢?为什么我们非要在非阻塞上套上一个Reactive?Reactive在这里解决了什么问题?

1.2 异步非阻塞编程

看到这里,应该能体会到非阻塞的"妙处"了,但是光有NIO是不够的,我们需要一个异步非阻塞编程范式,为什么呢?因为我们还有业务逻辑的处理啊,拿上面用户登陆的例子来说,一个登陆步骤如下:

  1. 验证A用户请求是否合法
  2. 网络调用查找A用户账号密码
  3. 验证账号密码是否一致

其中第一步和第三步因为是CPU计算,是比较快的操作,由主线程来做,第二个操作是IO操作,由另外IO线程来做(如果是select多路复用模型,此时IO线程就负责注册读写事件和调用select方法获取已经到来的读写事件),传统异步编程(传统,略带贬义的词意可以看出这种编程模型存在一定缺陷)我们可以想到的方案是Callbacks回调和Future非阻塞提交的方案来做。

传统的异步编程

Callbacks

耳熟能详的Swing注册监听器,是典型的对于事件流进行响应的回调异步机制,个人感觉是Reactive鼻祖

使用回调方案

// 主线程线程池
ExecutorService mainThread = Executors.newFixedThreadPool(8);

// 验证A用户请求是否合法
doCheckRequest();
// 非阻塞调用,加入回调动作
httpRequest().addCallBacks(new Callback<Response>() {
        public void onSuccess(Response response) { 
					mainThread.submit(() ->{
            	// 主线程做
          	  checkUserInfo(response);
        	});
        }

        public void onError(Throwable error) { 
					// 异常处理
        }
      });
// 非阻塞,立即往下走了
// 接着处理B用户的请求...

看起来确实能完成异步编程

Future

这里我们使用CompletableFuture更好的完成操作

// 主线程线程池
ExecutorService mainThread = Executors.newFixedThreadPool(8);

// 验证A用户请求是否合法
doCheckRequest();
// 非阻塞调用,加入回调动作
httpRequest().thenComposeAsync(response -> {
					mainThread.submit(() ->{
            	// 主线程做
          	  checkUserInfo(response);
        	});
      });
// 非阻塞,立即往下走了
// 接着处理B用户的请求...

似乎也能完成异步编程。

但这里我们的场景十分简单,如果是复杂的异步编程场景呢?这里举出Reactor文档中的一个例子:

想象一个场景:我们现在需要展示用户的前5名喜欢的东西在UI上,如果她没有喜欢的东西,那么就推荐给TA,这个场景需要3个服务(第一个是提供喜爱的物品ID列表,第二个是获取喜爱东西的细节信息,第三个是推荐物品细节给用户)

我们使用Callbacks来完成这个例子:

userService.getFavorites(userId, new Callback<List<String>>() { 
  // 获取喜爱物品ID列表的回调动作
  public void onSuccess(List<String> list) { 
    // 如果是空,我们给TA推荐物品
    if (list.isEmpty()) { 
      suggestionService.getSuggestions(new Callback<List<Favorite>>() {
        // 需要获取推荐细节,这里依然回调动作
        public void onSuccess(List<Favorite> list) { 
          // 这里拿到推荐细节,就可以放到UI上展示了
          UiUtils.submitOnUiThread(() -> { 
            list.stream()
                .limit(5)
                .forEach(uiList::show); 
            });
        }

        // 错误处理
        public void onError(Throwable error) { 
          UiUtils.errorPopup(error);
        }
      });
    } else {
      // 不为空,那么我们开始展示TOP5物品
      list.stream() 
          .limit(5)
          .forEach(favId -> favoriteService.getDetails(favId, 
            new Callback<Favorite>() {
              // 异步获取物品细节的回调动作
              public void onSuccess(Favorite details) {
                UiUtils.submitOnUiThread(() -> uiList.show(details));
              }
              // 错误处理
              public void onError(Throwable error) {
                UiUtils.errorPopup(error);
              }
            }
          ));
    }
  }
  // 错误处理
  public void onError(Throwable error) {
    UiUtils.errorPopup(error);
  }
});

仅仅是这么一点的业务逻辑,就需要写这么多回调接口,可读性极差,编程难度极高,后期将难以维护,我们称之为回调地狱(Callback Hell)

Swing编程的EventListener也是类似的回调地狱

如果我们使用Reactor做同样的事情呢?

// 获取喜爱物品的ID列表
userService.getFavorites(userId) 
           .flatMap(favoriteService::getDetails)  // 将每一个物品都获取其细节
           .switchIfEmpty(suggestionService.getSuggestions()) // 如果ID为空,上面的flatMap不会触发,这里就会触发为空动作,获取建议
           .take(5) // 获取前5的信息
           .publishOn(UiUtils.uiThreadScheduler()) // 交给主UI线程去做
           .subscribe(uiList::show, UiUtils::errorPopup); // 开始触发整个事件流,展示数据流最后的数据到UI上

如果我们需要增加异常处理?增加一行代码即可

.onErrorResume(throwable -> {
  // handle error
})

如果我们需要限制getFavorites这个接口最多耗时800ms,超时要抛出异常,怎么做?

userService.getFavorites(userId)
           .timeout(Duration.ofMillis(800)) // 800ms的超时处理
           .onErrorResume(cacheService.cachedFavoritesFor(userId)) // 异常处理
           .flatMap(favoriteService::getDetails) 
           .switchIfEmpty(suggestionService.getSuggestions())
           .take(5)
           .publishOn(UiUtils.uiThreadScheduler())
           .subscribe(uiList::show, UiUtils::errorPopup);

只需要增加两行代码,可读性,操作可组合性都提升了,编程难度相对于Callbacks形式大幅下降

那么Future在复杂编程中表现如何呢?

Future会逼callbacks好上那么一点点,即使Java8推出了CompletableFuture,但仍然存在操作组合不太好做,组合多个Future对象编排在一起并不容易

  • 调用get()方法还是会在其他线程中阻塞
  • 不支持懒计算
  • 缺少多值数据流的处理,和一个比较好的错误处理机制

考虑另一个例子:我们获取一个ID列表,然后根据ID获取名称和统计信息,将这两个信息成对组合为一个信息,使用CompletableFuture来完成这个例子:

// 获取ID列表
CompletableFuture<List<String>> ids = ifhIds(); 

CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> { 
  // 获取到ID列表后
	Stream<CompletableFuture<String>> zip =
			l.stream().map(i -> { 
    		// 遍历ID列表
    		// 根据ID获取名称
				CompletableFuture<String> nameTask = ifhName(i); 
				// 根据ID获取统计信息
				CompletableFuture<Integer> statTask = ifhStat(i); 

    		// 组合在一起
				return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat); 
			});
  // 将结果整成List
	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()
			// 阻塞等待future完成
			.map(CompletableFuture::join) 
			.collect(Collectors.toList()));
});

List<String> results = result.join(); 
assertThat(results).contains(
		"Name NameJoe has stats 103",
		"Name NameBart has stats 104",
		"Name NameHenry has stats 105",
		"Name NameNicole has stats 106",
		"Name NameABSLAJNFOAJNFOANFANSF has stats 121");

CompletableFuture组合操作虽然比callback好一些,但还是比较复杂,各自转换来转换去,然后才可以组合,我们来看看同等组合操作使用Reactor的表现:

Flux<String> ids = ifhrIds(); 
// Flux表示发射多次值,每次发送一个ID
Flux<String> combinations =
  	// 每一个ID都做如下操作
		ids.flatMap(id -> { 
      // 获取ID对应的name
			Mono<String> nameTask = ifhrName(id); 
      // 获取ID对应的stat
			Mono<Integer> statTask = ifhrStat(id); 

      // 组合
			return nameTask.zipWith(statTask, 
					(name, stat) -> "Name " + name + " has stats " + stat);
		});

// 结果转成一个List
Mono<List<String>> result = combinations.collectList(); 

List<String> results = result.block(); 
assertThat(results).containsExactly( 
		"Name NameJoe has stats 103",
		"Name NameBart has stats 104",
		"Name NameHenry has stats 105",
		"Name NameNicole has stats 106",
		"Name NameABSLAJNFOAJNFOANFANSF has stats 121"
);

可以看出来,在Reactor中所有结果都是Mono或者Flux,想要组合在一起异常简单,面向Mono或Flux组合即可。

到这里可以看出,使用CallBacks或Future进行异步编程都分别有很多缺陷,这些缺陷就是reactive的目标想要去解决的。reactive使用消息发布和对消息响应消费机制来完善这些缺陷。

我们需要reactive

引用自Reactor库文档

Reactive libraries, such as Reactor, aim to address these drawbacks of “classic” asynchronous approaches on the JVM while also focusing on a few additional aspects:

  • Composability and readability
  • Data as a flow manipulated with a rich vocabulary of operators
  • Nothing happens until you subscribe
  • Backpressure or the ability for the consumer to signal the producer that the rate of emission is too high
  • High level but high value abstraction that is concurrency-agnostic

Reactive库,例如Reactor,就是为了去解决"传统"异步编程的缺陷,主要致力于如下几点构建:

  • 操作可组合性和可读性
  • 数据可以像一个流一样操作,并且有丰富的操作符使用
  • 在调用subscribe订阅之前,没有事情会发生(说明了一个懒计算机制,以及声明式编程的特点)
  • 背压,控制发布数据端过快消费数据端过慢不平衡的问题
  • 屏蔽底层并发细节

其中最后一点可能暂时现在体会不大,但异步编程的一个特点就在于同样一个流程ABCD做下来,有可能是a线程做的A流程,接着给b线程做的B流程,一个请求的处理不一定都是由同一个线程来做的,此时内部数据流传递的时候会有一些并发问题,Reactive库屏蔽了这些并发问题(就像上面例子的ID列表的流式传递,就不需要考虑ID列表随着流传递,发射到下游之后下游线程是否可见等等问题)。

开头也说到,Reactive是一个设计模式,观察者模式的扩展,类比与迭代器模式的拉取数据(next方法拉取数据)的方式,Reactive更像是一个等待被推数据的过程,观察者观察数据的到来,之后对数据作出响应,响应式名称就由此而来,这是一个被动的过程,所以对于生产者生产消息的速率是不可控的,如果过快,消费者消费速率跟不上就会出现问题,因此Reactive引入了背压技术,可以使用多种途径对生产者生产消息做一个控制,从而达到平衡两者速率的效果。

2. Reactive的定义

最后,我们结合一些文献,尝试给Reactive下一个定义

  • 引用维基百科:

Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. This means that it becomes possible to express static (e.g. arrays) or dynamic (e.g. event emitters) data streams with ease via the employed programming language(s).

— https://en.wikipedia.org/wiki/Reactive_programming

这里提到的关键在于,Reactive是一个异步编程范式,可以很好处理数据流的变化传播(propagation of change)

  • 引用Spring5文档:

The term, “reactive,” refers to programming models that are built around reacting to change — network components reacting to I/O events, UI controllers reacting to mouse events, and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available.

There is also another important mechanism that we on the Spring team associate with “reactive” and that is non-blocking back pressure. In synchronous, imperative code, blocking calls serve as a natural form of back pressure that forces the caller to wait. In non-blocking code, it becomes important to control the rate of events so that a fast producer does not overwhelm its destination.

Reactive Streams is a small spec (also adopted in Java 9) that defines the interaction between asynchronous components with back pressure. For example a data repository (acting as Publisher) can produce data that an HTTP server (acting as Subscriber) can then write to the response. The main purpose of Reactive Streams is to let the subscriber control how quickly or how slowly the publisher produces data.

Spring5认为,reactive是一个围绕变化响应的编程模型,变化响应指的是对一些IO事件(NIO)或者是例如鼠标事件等等。在这种层面上,非阻塞指的就是reactive,而不是被阻塞,reactive是可以在操作完成数据可用对通知作出反应的模式。

  • 引用Reactor库文档:

The reactive programming paradigm is often presented in object-oriented languages as an extension of the Observer design pattern. You can also compare the main reactive streams pattern with the familiar Iterator design pattern, as there is a duality to the Iterable-Iterator pair in all of these libraries. One major difference is that, while an Iterator is pull-based, reactive streams are push-based.

Reactor从实现角度去说明,reactive类比迭代器模式,前者是推数据模式,后者是主动拉数据模式,应对数据流作出响应则为响应式编程

此时我们知道,Reactive是具有如下特点的:

  • 实现模式
    • Reactive Programming 作为观察者模式(Observer) 的延伸,不同于传统的命令编程方式( Imperative programming)同步拉取数据的方式,如迭代器模式(Iterator) ,而是采用数据发布者同步或异步地推送到数据流(Data Streams)的方案。
  • 响应式
    • 围绕变化响应进行,变化可以是传播数据流、传播事件(IO事件、鼠标事件等),当该数据流(Data Steams)订阅者监听到传播变化时,响应通知方面,包括操作完成(operations complete)和数据可用(data becomes available)。立即作出响应动作。
  • 可读性好、可组合的、丰富的数据操作,进一步简化异步编程的难度(屏蔽并发细节)
    • 在实现层面上,Reactive Programming 可结合函数式编程简化面向对象语言语法的臃肿性,屏蔽并发实现的复杂细节,提供数据流的多种多样的操作,从而达到提升代码的可读性,以及减少 Bugs 出现的目的。
  • 背压支持
    • Reactive Programming 结合背压(Backpressure)的技术解决发布端生成数据的速率高于订阅端消费的问题。

希望达到使用少量的系统资源,提升应用的伸缩性的目的,我们需要使用异步编程。而响应式的推模式编程确实能够解决传统异步编程的各项缺点。使用Reactive这种声明式的异步编程范式提升应用伸缩性,在我看来是之后的一种大趋势。

3. Reactive适用性

Reactive异步非阻塞编程虽好,但目前也存在一些挑战,并不是人人都能做的,这里引用Spring5官方文档的关于SpringMVC和SpringWebFlux切换建议:

A simple way to evaluate an application is to check its dependencies. If you have blocking persistence APIs (JPA, JDBC) or networking APIs to use, Spring MVC is the best choice for common architectures at least. It is technically feasible with both Reactor and RxJava to perform blocking calls on a separate thread but you would not be making the most of a non-blocking web stack.

Spring5这里提到的观点是,如果你有阻塞的API例如JDBC或者一些网络调用,SpringMVC则是你的最佳选择,就像文章中间我举的一个现实生产中的一个例子一样,阻塞在WebFlux中相当影响吞吐量,所以不管是数据库调用、Redis调用、微服务之间潜在的网络调用,都需要使用Reactive,目前不知道JDBC是否有Reactive版本,Redis已经有Spring封装lettuce的API做的Reactive版本,Feign则需要寻找第三方开源人士提供的Reactive版本,所以我想说的是,如果你找不到对应的Reactive版本的API,那就不要使用WebFlux构建你的系统,吞吐量会降低,因为这样会阻塞主线程。很多人不知道这一点的关键性,盲目使用Gateway然后发现自己的吞吐量很低,开始吐槽Gateway或是reactive没有传闻中那么神,此时更需要想想系统中是否存在潜在的阻塞API。

If you have a large team, keep in mind the steep learning curve in the shift to non-blocking, functional, and declarative programming. A practical way to start without a full switch is to use the reactive WebClient. Beyond that, start small and measure the benefits. We expect that, for a wide range of applications, the shift is unnecessary. If you are unsure what benefits to look for, start by learning about how non-blocking I/O works (for example, concurrency on single-threaded Node.js) and its effects.

这里提到的关键点在于学习路线相当陡峭(steep learning curve in the shift to non-blocking),最基本的你需要知道非阻塞IO的作用,知道Spring的组件WebClient的使用,还得学习Reactive丰富的操作符,这样才可以用的好WebFlux,可谓是高端玩家的游戏,其对于团队的整体能力要求还是很高的。此时你需要评估学习成本和带来的收益,如果不是非常高并发的系统,我这里还是建议传统的SpringMVC。

If you have a Spring MVC application that works fine, there is no need to change. Imperative programming is the easiest way to write, understand, and debug code. You have maximum choice of libraries, since, historically, most are blocking.

最后,还是那句话,如果你的SpringMVC应用可以良好工作,就没必要更换WebFlux。使用过Reactive的小伙伴一定知道调试和查看Reactive的异常堆栈是一件很困难的事情。传统命令式编程还是更容易写、理解和debug的,所使用的阻塞API的类库选择很多并且都已经成熟。

想入Reactive这个大坑,准备好深入学习理解再说吧,不然很容易得不偿失。

4. 参考文献

  • 《Reactive Programming 一种技术,各自表述》 https://www.imooc.com/article/46306
  • 《Reactor库官方文档》https://projectreactor.io/docs/core/release/reference/#intro-reactive
  • 《Spring5官方文档》https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-why-reactive
  • 《维基百科Reactive_Programming》https://en.wikipedia.org/wiki/Reactive_programming
  • 《reactive-streams-jvm》https://github.com/reactive-streams/reactive-streams-jvm
  • 《The Reactive Manifesto》https://www.reactivemanifesto.org/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章