17.1 反应式宣言
反应式宣言由Jonas Bonér、Dave Farley、Roland Kuhn和Martin Thompson在2013年至2014年间发起,它定义了一套开发反应式应用和系统的规范。该宣言指出了反应式应用的四个典型特征。
- 响应性——顾名思义,反应式系统的响应时间很快,更重要的是它的响应时间应该是稳定且可预测的。只有这样,用户才能明确地设定他的预期。而这反过来又会增强用户的信心,是应用易用性的关键指标。
- 韧性——系统在出现失败时依然能继续响应服务。为了构建弹性的应用,反应式宣言提供了一系列的建议,包括组件运行时复制,从时间(发送方和接受方都拥有相互独立的生命周期)和空间(发送方和接收方运行于不同的进程)维度对组件进行解耦,从而使任何一个组件都能以异步的方式向其他组件分发任务。
- 弹性——影响应用响应性的另一个重要因素是应用的工作负载。应用生命周期中不可避免地会遭遇各种规模的负载。反应式系统在设计时就需要考虑这一点,增加分配的资源后,受影响的组件要有能力自动地适配和服务更大的负荷。
- 消息驱动——韧性和弹性要求明确定义构成系统的组件之间的边界,从而确保组件间的松耦合、组件隔离以及位置透明性。跨组件通信则通过异步消息传递。这种设计既实现了韧性(以消息传递组件失败)又确保了弹性(通过监控交换消息规模的变化,适时调整资源分配,从而实现资源配置的优化,满足业务的需求)
图17-1展现了这四个特征之间的相互依赖关系。这些原则适用于各种规模的项目,无论是搭建小型应用内部架构还是选择用什么策略协调各个应用来构建一个大型系统。关于应用这些思想的细节,尤其是如何界定组件的粒度,还需要进一步的讨论。

图 17-1 反应式系统的关键特征
17.1.1 应用层的反应式编程
对应用层组件而言,反应式编程的主要特征使得任务能以异步的方式运行。以异步非阻塞方式处理事件流对充分利用现代多核处理器至关重要,或者更确切地说,这一技术让线程尽可能地竞争处理器的使用权。为了达到这一目的,反应式编程框架和库会在轻量级的结构,譬如Future、Actor或者更常见的事件循环间共享线程(相对昂贵且稀缺的资源),以分发回调函数的结果,最终实现对事件处理结果的收集、转换和管理。
背景知识调查
如果你对事件、消息、信号以及事件循环(或者叫“发布–订阅”、监听,以及本章后续会提到的背压)感到困惑,请转去阅读第15章中的相关内容。如果你没有任何不适,那么请继续阅读。
这些技术不仅比线程更轻量级,对开发者而言,还有更大的诱惑:它们提升了创建并发以及异步应用的抽象层次,如此一来开发者就能更关注于业务需求,不必花费大量精力在像同步、竞争条件、死锁这样典型的多线程底层实现上。
采用这种线程多路复用策略时需要特别注意一点:不要在主事件循环中添加可能阻塞的操作。提到阻塞操作,这里特别要关注的是所有I/O密集型的操作,譬如访问数据库或文件系统,或者调用远程服务,这些都是可能消耗比较长时间的事件,甚至无法预测何时能够结束。下面我们用一个实际的例子来解释为何你应该在线程多路复用时避免阻塞操作,这样可能更生动直观一些。
设想有这样一个典型多路复用的简单场景,这个场景中你需要创建一个两线程的线程池,处理来自三个流的事件。由于同一时刻只能处理两个流,只有通过竞争,流才能高效公平地共享那两个线程。现在假设其中一个流中,某个事件触发了一个可能很慢的I/O操作,譬如向文件系统写入数据,或者调用阻塞式API从数据库中拉取数据。如图17-2所示,在这种情况下,线程2由于需要等待I/O操作完成,傻傻地阻塞在那里,无法继续执行有意义的工作。此时线程1还在处理第一个流的数据,阻塞操作完成之前,第三个流完全没有机会被处理。

图 17-2 阻塞操作让线程进入闲等状态,其他的计算也无法获得执行机会
为了解决这一问题,大多数的反应式框架(譬如RxJava和Akka)中都可以开辟独立的线程池用于执行阻塞式操作。主线程池中运行的线程执行的都为无阻塞的操作,以确保所有的CPU核都能得到最充分的利用。为CPU密集型和I/O密集型的操作分别创建单独的线程池还有更深层的好处,你可以更精细地监控不同类型任务的性能,从而更好地配置和调整线程池的规模,更好地适应业务的需求。
通过遵循反应式原则开发应用只是反应式编程的一小部分,很多时候甚至不是最困难的部分。将一系列反应式应用整合成一个协调良好的交互式系统与设计一个独立高效运行的反应式应用比较起来,其重要程度不相上下。
17.1.2 反应式系统
反应式系统是一种新型软件架构,应用这种架构多个独立应用可以像一个单一系统那样步调一致地工作,同时其又具备良好的扩展性,构成反应式系统的各个应用也是充分解耦的,因此,即使其中某一个应用发生失效,也不会拖垮整个系统。反应式应用与反应式系统的主要区别是,前者主要对临时数据流进行处理,因此其工作模式被称为事件驱动型。而后者主要用于构造应用以及协调组件间的通信。具备这种特征的系统通常会被称为消息驱动系统。
消息驱动与事件驱动的另一个重要区别是,消息往往是直接发送给某个单一目标的,而事件会被所有注册了该事件的组件接收。此外,还有一点非常重要,值得特别提一下,反应式系统中消息是以异步的方式发送和接收的,这种方式有效地解耦了发送方与接收方。组件间完全的解耦合既是实现有效隔离的必要前提,也是保障系统在遭遇失效(韧性)和超大负荷(弹性)时仍能保持响应的基础。
更确切地说,反应式架构的韧性是凭借将失效隔离在组件内部,避免故障传递到临接的组件来实现的,如果不加控制的话,这种灾难传递可能会毁掉整个系统。从反应式系统角度而言,韧性更偏向于容错。系统不只要能优雅地降级,更重要的是能通过隔离失效组件,将系统重新拉回健康状态。这种神奇的魔力来自于将失效控制在一个范围内,并将这些失效作为消息传递给管理组件。通过这种方式,失效节点的管理可以不受失效组件自身的影响,在一个安全的上下文中进行。
位置透明性之于韧性与隔离和解耦之于弹性一样至关重要,是反应式系统实现韧性的决定性要素。基于位置透明性,反应式系统的所有组件都可以和其他任何服务通信,无须顾忌接收方在什么位置。位置透明性使得系统能够依据当前的负荷情况,对应用进行复制或者自动地水平扩展。这种位置无关的扩展也是反应式应用(异步、并发、即时松耦合)与反应式系统(凭借位置透明性从空间角度解耦)之间的另一个区别。
本章接下来的内容会带领大家通过几个实例来学习反应式编程,此外,我们会着重介绍Java 9的Flow API。
