我们知道,系统面对大流量、高并发的访问请求时,就可能会出现一系列性能问题,导致服务丧失了即时的响应性。如何时刻确保系统具有应对请求压力的能力,是架构设计的核心问题之一。
经典的服务隔离、限流、降级以及熔断等机制能够在一定程度上确保系统的响应性。但这些机制更多的是从系统架构和应用部署的角度出发解决问题,而不是编程技术本身。今天我们要介绍的是构建系统响应性的一种崭新的解决方案,这就是响应式编程(Reactive Programming)。
我们知道,传统的编程模型采用的是同步阻塞式(Blocking)的请求响应过程,这是现有各种经典解决方案所不得不面对的一种限制。 而响应式编程打破了这种限制,采用了异步非阻塞式(Non-Blocking)的编程模型,从而提高服务的响应能力。
这里提到了同步阻塞和异步非阻塞这两个核心概念,正确理解这两个概念是你掌握响应式编程的前提条件。所以接下来,我们就来看看响应式编程技术是如何基于它们诞生的。
为什么需要响应式编程?
如果你使用 Spring 框架开发过 Web 应用程序,那么你一定对下面这种开发方式非常熟悉:
vbnet
public Order getRemoteOrderByOrderNumber(String orderNumber) {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Order> result= restTemplate.exchange(
"http://orderservice/orders/{orderNumber}", HttpMethod.GET, null, Order.class, orderNumber);
Order order= result.getBody();
processOrder(order);
return order;
}
这是一个查询订单(Order)信息的应用场景,我们使用了 Spring 中的 RestTemplate 模板工具类,通过该类所提供的 exchange() 方法对远程 Web 服务所暴露的 HTTP 端点发起了请求。
这种实现方式在日常开发中非常有代表性, 基于 Spring Cloud 开发的微服务系统,本质上,也是通过这种方式完成服务与服务之间的远程调用。但是,这个方法实际上存在明显的缺陷,因为处理过程是阻塞式的。
正是因为同步阻塞的存在才导致了异步非阻塞相关技术的诞生和发展,进而才有了今天要介绍的响应式编程技术。那么,究竟什么是阻塞式呢?
同步阻塞
我们首先来分析代码中的线程模型,看看问题出在哪里。为了更好的分析整个调用过程,我们假设服务的提供者为服务 A,而服务的消费者为服务 B,那么这两个服务的交互过程应该是这样的。

服务 A 和服务 B 的交互过程图
可以看到,当服务 B 向服务 A 发送 HTTP 请求时,线程 B,只在发起请求和响应结果的一小部分时间内有效使用 CPU,而更多时间则是在阻塞式地等待来自服务 A 中线程的处理结果。显然,整个过程的 CPU 利用效率是很低的,很多时间被浪费在了 I/O 阻塞上,无法执行其他处理过程。
更进一步,我们继续分析服务 A 中的处理过程。
如果我们采用典型的三层架构,那么沿着 Web 服务层->业务逻辑层->数据访问层整个调用链路,每一步的操作过程都存在着前面描述的线程等待问题。也就是说,整个技术栈中的每一个环节都可能是同步阻塞的。
这样的话,整个调用链路的资源利用率都会变低,导致请求的处理过程出现延迟,而丧失了我们想要的即时响应性。

Web 应用程序三层架构
异步非阻塞
为了解决同步阻塞问题,可以引入异步非阻塞的相关技术。异步非阻塞技术能够通过多线程技术,将整个请求处理过程交由不同线程并行处理,提高了系统资源利用率。
在 Java 世界中,一般会采用回调(Callback)和 Future 这两种机制,但这两种机制都存在一定局限性:
回调的核心问题在于,处理过程会形成一种嵌套结构,给代码的开发和调试带来很大的挑战。
Future 机制本质上是一种多线程技术,大量线程之间的相互协作需要频繁进行上下文切换,同样会导致资源利用效率低下。
其实引入响应式编程技术,我们就可以很好地解决这种类型的问题。
响应式编程采用全新的响应式数据流(Stream),实现异步非阻塞式的网络通信和数据访问机制,能够减低不必要的线程等待时间。那么,所谓的响应式编程到底是什么样子的呢?
什么是响应式编程?
响应式编程技术的核心是数据流,而数据流又是构建在传统的事件驱动架构与发布订阅模式之上。在讲解响应式编程技术之前,我们先来看一下发布订阅模式和事件处理相关的技术体系。
发布订阅模式和事件处理
相信你应该对设计模式中经典的观察者模式不陌生。观察者模式拥有一个主题(Subject)以及针对这个主题的一个依赖者列表,这些依赖者被称为观察者(Observer)。
而发布订阅(Publish-Subscribe)模式可以认为是对观察者模式的一种改进。在这种模式中,发布者和订阅者相互之间可以没有直接的依赖关系,而是通过发送事件到事件处理平台上完成整合。
针对开头提到的订单查询操作,我们可以基于发布订阅模式重构流程。通过构建发布订阅模式以及事件处理平台,我们具备了传播和处理异步事件的能力,从而为实现响应式编程提供了基础。(图 3)

发布 - 订阅模式下的订单信息获取过程
再来看单个服务的内部,在三层架构中整个调用链路同样可以用发布订阅模式来重构。这时,数据库中的数据一有变化就会通知到上游组件,而不是上游组件通过主动拉取的方式获取数据。这样做相当于,让处于调用链路中的各个组件由同步调用转化为了异步调用,图中的虚线和箭头方向表达了这层含义。

基于响应式实现方法的数据流转时序图
数据流和响应式
显然,上图中异步事件传播的思想可以扩展到整个系统。
你可以想象系统中会存在着很多类似 OderEvent 这样的事件。每一种事件会被用户的操作或者系统自身的行为触发,并形成了事件的集合。我们可以把这个集合看成是一串串连起来的数据流,而系统的响应能力就体现在对这些数据流的即时响应过程上。

全流程数据流示意图
对于技术实现过程而言,数据流是一个全流程的概念。也就是说,无论是底层数据库、服务层、Web 服务层,或是在这个流程中所包含的任意中间层组件,整个数据传递链路都应该采用事件驱动的方式来运作。这样,我们就可以不用传统的同步调用的方式来处理数据,而是由处于全流程中的各层组件自行执行事件,实现了全流程的异步非阻塞处理机制。这就是响应式编程的核心特点。
相较传统开发普遍采用的"拉"模式,在响应式编程下,基于事件的触发和订阅机制,这就形成了一种类似"推"的工作方式。

推模式下的数据流处理方式示意图
这种工作方式的优势就在于,生成事件和消费事件的过程是异步执行的,所以线程的生命周期都很短,也就意味着资源之间的竞争关系较少,服务器的响应能力也就越高。这就是响应式编程的精髓,也是解决系统性能问题的关键所在。
讲到这里,你可能会问,我们如何来使用响应式编程技术来开发业务系统呢?不用担心,到目前为止,业界已经诞生了诸如 RxJava、Project Reactor、Akka 等一大批优秀的响应式编程框架。
而在 Spring 5 中,也引入了 WebFlux、Reactive Spring Data 等新一代的编程组件来实现响应式 Web 服务和响应式数据访问。这种框架和工具,可以很好的解决传统同步阻塞式处理方式所存在的性能问题。
总结
今天我们系统分析了传统服务调用存在的问题,从而引出响应式编程概念和实现方法。
从技术演进的过程和趋势而言,响应式编程的出现有其必然性。
但是响应式编程也不是一种完全颠覆式的技术体系,而是在现有的异步调用、观察者模式、发布订阅模式等的基础上发展起来的一种全新的编程模式,能够给系统带来即时响应性的优点。