Skip to content

Files

Latest commit

8814ef5 · Oct 11, 2021

History

History
1203 lines (905 loc) · 68.9 KB

File metadata and controls

1203 lines (905 loc) · 68.9 KB

十六、反应式系统

在本书的最后一章中,我们将打破相关叙述的流程,更接近现实生活中的专业编程。随着处理的数据越来越多,服务越来越复杂,对更具适应性、高度可扩展性和分布式应用程序的需求呈指数级增长。这就是我们将在本章中讨论的,这样一个软件系统在实践中的外观。

在本章中,我们将介绍以下主题:

  • 如何快速处理大量数据
  • 微服务
  • 反应系统
  • 练习-创建io.reactivex.Observable

如何快速处理大量数据

有许多可测量的性能特征可应用于应用程序。使用哪一个取决于应用程序的目的。它们通常被列为非功能性需求。最典型的一套包括以下三种:

  • 吞吐量:单位时间内处理的请求数。
  • 延迟:从提交请求到接收到响应的第一个字节之间经过的时间。它是以秒、毫秒等为单位测量的。
  • 内存占用:应用程序消耗的最小、最大或平均内存量。

实际上,延迟通常被计算为吞吐量的倒数。这些特性随着负载的增长而变化,因此非功能性需求通常包括平均负载和最大负载下每个特性的最大值。

通常,吞吐量和延迟的提高只是以牺牲内存为代价,除非添加更快的 CPU 可以改善这三个特性。但这取决于处理的性质。例如,与低性能设备的输入/输出(或其他交互)可以施加限制,并且代码中的任何更改都不能提高应用程序的性能。

在测量每一个特征时也有细微的差别。例如,我们可以使用 99%的最快(最小延迟)请求中的最大延迟,而不是将延迟测量为所有请求的平均值。否则,它看起来就像是亿万富翁和收入金字塔底部的人的财富除以 2 得到的平均财富数。

评估应用程序性能时,必须回答以下问题:

  • 是否可以超过请求的延迟上限?如果是,多久一次,增加多少?
  • 不良延迟的时间可以有多长,允许发生的频率有多高?
  • 谁/什么衡量生产中的延迟?
  • 预计峰值负荷是多少?预计持续多长时间?

只有在所有这些(以及类似的问题)都得到回答,并且非功能性需求已经确定之后,我们才能开始设计系统,测试它,调整,然后再次测试。有许多编程技术被证明能够在可接受的内存消耗下有效地实现所需的吞吐量。

在此背景下,术语异步非阻塞分布式可伸缩反应性响应性弹性弹性消息驱动变得无处不在,只是高性能的同义词。我们将讨论这些术语中的每一个,以便读者能够理解将微服务反应系统带入生活的动机,我们将在本章的下两部分中介绍。

异步的

异步表示请求方立即得到响应*,但结果不存在。相反,请求者接收一个对象,其方法允许我们检查结果是否准备好。请求者定期调用此方法,当结果准备好时,使用同一对象上的另一个方法检索该方法。*

这种解决方案的优点是,请求者可以在等待时做其他事情。例如,在第 11 章JVM 进程和垃圾收集中,我们演示了如何创建子线程。因此,主线程可以创建一个子线程,该子线程发送非异步(也称为阻塞)请求,并等待其返回,而不执行任何操作。同时,主线程可以继续执行其他操作,定期调用子线程对象以查看结果是否就绪。

这是最基本的异步调用实现。事实上,我们已经在处理并行流时使用了它。并行流操作在幕后创建子线程,将流分解为多个段,并将每个段分配给专用线程,然后在最后一个段中聚合每个段的结果。在上一章中,我们编写了执行聚合任务的函数。提醒一下,这些函数称为组合器。

让我们比较一下处理顺序流和并行流时相同功能的性能。

顺序流与并行流

为了演示顺序处理和并行处理之间的区别,让我们设想一个从 10 个物理设备(传感器)收集数据并计算平均值的系统。此类系统的界面可能如下所示:

interface MeasuringSystem {
    double get(String id);
}

它只有一种方法get(),接收传感器 ID 并返回测量结果。使用这个接口,我们可以实现许多不同的系统,这些系统能够调用不同的设备。出于演示目的,我们不打算编写大量代码。我们所需要的只是设置 100 毫秒的延迟(模拟从传感器收集测量值所需的时间)并返回一些数字。我们可以按如下方式实施延迟:

void pauseMs(int ms) {
    try{
        TimeUnit.MILLISECONDS.sleep(ms);
    } catch(InterruptedException ex){
        ex.printStackTrace();
    }
}

至于得到的数字,我们将使用Math.random()来模拟从不同传感器接收到的测量值的差异(这就是为什么我们需要找到一个平均值来抵消单个设备的误差和其他特性)。因此,我们的演示实现可能如下所示:

class MeasuringSystemImpl implements MeasuringSystem {
    public double get(String id){
         demo.pauseMs(100);
         return 10\. * Math.random();
    }
}

现在,我们意识到我们的MeasuringInterface是一个功能接口,因为它只有一种方法。这意味着我们可以使用java.util.function包中的一个标准功能接口;即Function<String, Double>

Function<String, Double> mSys = id -> {
    demo.pauseMs(100);
    return 10\. + Math.random();
};

因此,我们可以放弃我们的MeasuringSystem接口和MeasuringSystemImpl类。但我们可以留下mSys测量系统)识别器,它反映了此功能背后的理念:它代表一个测量系统,提供对其传感器的访问,并允许我们从传感器收集数据。

现在,让我们创建一个传感器 ID 列表:

List<String> ids = IntStream.range(1, 11)
        .mapToObj(i -> "id" + i).collect(Collectors.toList());

同样,在现实生活中,我们需要收集真实设备的 ID,但出于演示目的,我们只是生成它们。

最后,我们将创建collectData()方法,该方法调用所有传感器并计算所有接收数据的平均值:

Stream<Double> collectData(Stream<String> stream, 
                         Function<String, Double> mSys){
    return  stream.map(id -> mSys.apply(id));
}

如您所见,该方法接收一个提供 ID 的流和一个使用每个 ID 从传感器获取测量值的函数。

下面是我们将如何使用getAverage()方法从averageDemo()方法调用此方法:

void averageDemo() {
    Function<String, Double> mSys = id -> {
         pauseMs(100);
         return 10\. + Math.random();
    };
    getAverage(() -> collectData(ids.stream(), mSys)); 
}

void getAverage(Supplier<Stream<Double>> collectData) {
    LocalTime start = LocalTime.now();
    double a = collectData.get()
                    .mapToDouble(Double::valueOf).average().orElse(0);
    System.out.println((Math.round(a * 100.) / 100.) + " in " + 
         Duration.between(start, LocalTime.now()).toMillis() + " ms");
}   

如您所见,我们创建了表示测量系统的函数,并将其与 ID 流一起传递到collectData()方法中。然后,我们创建SupplierStream<Double>>函数作为() -> collectData(ids.stream(), mSys)lambda 表达式,并将其作为collectData参数传递给getAverage()方法。在getAverage()方法内部,我们调用供应商的get(),从而调用collectData(ids.stream(), mSys),返回Stream<Double>。然后我们通过mapToDouble()操作将其转换为DoubleStream,这样我们就可以应用average()操作。average()操作返回一个Optional<Double>对象,我们称其为orElse(0)方法,该方法返回计算值或零(例如,如果测量系统无法连接到任何传感器并返回空流)。getAverage()方法的最后一行打印结果以及计算结果所需的时间。在实际代码中,我们将返回结果并将其用于其他计算。但是对于我们的演示,我们只是打印它。

现在,我们可以比较顺序流处理和并行流处理的性能:

List<String> ids = IntStream.range(1, 11)
              .mapToObj(i -> "id" + i).collect(Collectors.toList());
Function<String, Double> mSys = id -> {
        pauseMs(100);
        return 10\. + Math.random();
};
getAverage(() -> collectData(ids.stream(), mSys));    
                                             //prints: 10.46 in 1031 ms
getAverage(() -> collectData(ids.parallelStream(), mSys));  
                                             //prints: 10.49 in 212 ms

如您所见,处理并行流比处理顺序流快五倍。

虽然在幕后,并行流使用\异步处理,但这并不是程序员在谈论异步处理请求时的想法。从应用程序的角度来看,它只是并行(也称为并发)处理。它比顺序处理快,但主线程必须等待所有调用完成并检索所有数据。如果每个呼叫至少需要 100 毫秒(在我们的案例中就是这样),那么所有呼叫的处理就无法在更短的时间内完成。

当然,我们可以创建一个子线程,让它放置所有调用,并等待它们完成,而主线程执行其他操作。我们甚至可以创建一个这样做的服务,这样应用程序就可以告诉这样的服务做什么(在我们的例子中,传递传感器 ID),然后继续做其他事情。稍后,主线程可以再次调用该服务,并获得结果或在约定的位置获取结果。这将是程序员所谈论的真正的异步处理。

但是在编写这样的代码之前,让我们先看看位于java.util.concurrent包中的CompletableFuture类。它完成了我们描述的一切,甚至更多。

使用 CompletableFuture 类

使用CompletableFuture对象,我们可以将向测量系统发送数据请求(并创建CompletableFuture对象)与从CompletableFuture对象获取结果分开。这正是我们在解释什么是异步处理时描述的场景。让我们在代码中演示它。与我们向测量系统提交请求的方式类似,我们可以使用CompletableFuture.supplyAsync()静态方法:

List<CompletableFuture<Double>> list = ids.stream()
        .map(id -> CompletableFuture.supplyAsync(() -> mSys.apply(id)))
        .collect(Collectors.toList());

不同之处在于supplyAsync()方法不会等待对测量系统的调用返回。相反,它会立即创建一个CompletableFuture对象并返回该对象,这样客户机就可以随时使用该对象检索测量系统返回的值。还有一些方法允许我们检查是否返回了值,但这不是本演示的重点,演示如何使用CompletableFuture类来组织异步处理。

创建的CompletableFuture对象列表可以存储在任何地方。我们选择将其存储在Map中。事实上,我们已经创建了一种sendRequests()方法,可以向任意数量的测量系统发送任意数量的请求:

Map<Integer, List<CompletableFuture<Double>>> 
                  sendRequests(List<List<String>> idLists, 
                               List<Function<String, Double>> mSystems){
   LocalTime start = LocalTime.now();
   Map<Integer, List<CompletableFuture<Double>>> requests 
                                                       = new HashMap<>();
   for(int i = 0; i < idLists.size(); i++){
      for(Function<String, Double> mSys: mSystems){
         List<String> ids = idLists.get(i);
         List<CompletableFuture<Double>> list = ids.stream()
          .map(id -> CompletableFuture.supplyAsync(() -> mSys.apply(id)))
          .collect(Collectors.toList());
         requests.put(i, list);
      }
   }
   long dur = Duration.between(start, LocalTime.now()).toMillis();
   System.out.println("Submitted in " + dur + " ms");
   return requests;
}

如您所见,前面的方法接受两个参数:

  • List<List<String>> idLists:传感器 ID 列表的集合(列表),每个列表特定于特定测量系统。
  • List<Function<String, Double>> mSystems:测量系统列表,每个系统均以Function<String, Double>表示,具有一个apply()方法,该方法接受传感器 ID 并返回一个双倍值(测量结果)。此列表中的系统与第一个参数中的传感器 ID 列表的顺序相同,因此我们可以根据其位置将 ID 与系统匹配。

然后,我们创建Map<Integer, List<CompletableFuture<Double>>>对象来存储CompletableFuture对象的列表。我们在一个for循环中生成它们,然后将它们存储在一个Map循环中,其中的一个密钥只是一个序列号。Map返回给客户机,可以存储在任何地方,任何时间段(当然,有一些限制可以修改,但我们不在这里讨论)。稍后,当客户端决定获取请求的结果时,可以使用getAverage()方法检索它们:

void getAverage(Map<Integer, List<CompletableFuture<Double>>> requests){
    for(List<CompletableFuture<Double>> list: requests.values()){
        getAverage(() -> list.stream().map(CompletableFuture::join));
    }
}

前面的方法接受由sendRequests()方法创建的Map对象,并迭代Map中存储的所有值(ComputableFuture对象列表)。对于每个列表,它创建一个流,将每个元素(T4 的对象)映射到元素上调用的 T5 方法的结果。此方法检索从测量系统的相应调用返回的值。如果该值不可用,则该方法等待一段时间(可配置的值),然后退出(并返回null,或最终从对测量系统的调用中接收该值(如果可用)。同样,我们不打算讨论围绕故障设置的所有防护措施,以便将重点放在主要功能上。

() -> list.stream().map(CompletableFuture::join)函数实际上被传递到getAverage()方法中(您应该很熟悉),我们在处理上一个示例中的流时使用了该方法:

void getAverage(Supplier<Stream<Double>> collectData) {
    LocalTime start = LocalTime.now();
    double a = collectData.get()
                    .mapToDouble(Double::valueOf).average().orElse(0);
    System.out.println((Math.round(a * 100.) / 100.) + " in " + 
         Duration.between(start, LocalTime.now()).toMillis() + " ms");
}

此方法计算传入流发出的所有值的平均值,打印它,并捕获处理流所花费的时间(并计算平均值)。

现在,让我们使用新方法,看看性能是如何提高的:

Function<String, Double> mSys = id -> {
     pauseMs(100);
     return 10\. + Math.random();
 };
 List<Function<String, Double>> mSystems = List.of(mSys, mSys, mSys);
 List<List<String>> idLists = List.of(ids, ids, ids);

 Map<Integer, List<CompletableFuture<Double>>> requestLists = 
        sendRequests(idLists, mSystems);  //prints: Submitted in 13 ms

 pauseMs(2000);  //The main thread can continue doing something else
                 //for any period of time
 getAverage(requestLists);               //prints: 10.49 in 5 ms
                                         //        10.61 in 0 ms
                                         //        10.51 in 0 ms

为了简单起见,我们重用了相同的测量系统(及其 ID)来模拟使用三个测量系统。您可以看到,所有三个系统的请求都是在 13 毫秒内提交的,sendRequests()方法存在,并且主线程可以自由执行其他操作至少两秒钟。这是实际发送所有请求和接收响应所需的时间,因为pauseMs(100)用于测量系统的每次调用。然后,我们计算每个系统的平均值,几乎不需要时间。这就是程序员在谈论异步处理请求时的意思。

CompletableFuture类有很多方法,并且有其他几个类和接口的支持。例如,通过使用线程池,可以缩短收集所有数据的两秒暂停时间:

Map<Integer, List<CompletableFuture<Double>>> 
                  sendRequests(List<List<String>> idLists, 
                               List<Function<String, Double>> mSystems){
   ExecutorService pool = Executors.newCachedThreadPool();
   LocalTime start = LocalTime.now();
   Map<Integer, List<CompletableFuture<Double>>> requests 
                                                       = new HashMap<>();
   for(int i = 0; i < idLists.size(); i++){
      for(Function<String, Double> mSys: mSystems){
         List<String> ids = idLists.get(i);
         List<CompletableFuture<Double>> list = ids.stream()
          .map(id -> CompletableFuture.supplyAsync(() -> mSys.apply(id), 
 pool))
          .collect(Collectors.toList());
         requests.put(i, list);
      }
   }
   pool.shutdown();
   long dur = Duration.between(start, LocalTime.now()).toMillis();
   System.out.println("Submitted in " + dur + " ms");
   return requests;
}

有各种各样的这样的游泳池,用于不同的目的和不同的表演。但所有这些都不会改变整个系统设计,因此我们将省略这些细节。

因此,异步处理的功能非常强大。但谁能从中受益呢?

如果您创建了一个应用程序来收集数据并根据需要计算每个测量系统的平均值,那么从客户机的角度来看,它仍然需要很多时间,因为暂停(如果使用线程池,暂停时间为两秒或更短)仍然包含在客户机的等待时间中。因此,对于客户端来说,异步处理的优势就消失了,除非您设计了 API,以便客户端可以提交请求并走开去做其他事情,然后稍后再获取结果。

这就是同步(或阻塞】API 与异步API 的区别,前者是当客户端等待(阻塞)结果返回时,后者是当客户端提交请求并离开去做其他事情时,然后再获取结果。

异步 API 的可能性增强了我们对延迟的理解。通常,所谓延迟,程序员指的是在同一次调用 API 期间,从提交请求到收到响应的第一个字节之间的时间。但是,如果 API 是异步的,则延迟的定义将更改为“提交请求的那一刻以及客户端可以收集结果的时间”。在这种情况下,每次调用期间的延迟被假定为远小于发出请求的调用和收集结果的调用之间的时间。

还有一个非阻塞API 的概念,我们将在下一节中讨论。

非阻塞

对于应用程序的客户机,非阻塞 API 的概念只告诉我们,应用程序可能是可伸缩的、反应式的、响应式的、有弹性的、有弹性的和消息驱动的。在下面的章节中,我们将讨论所有这些术语,但现在,我们希望您能够从名称本身中了解它们的含义。

这种说法意味着两件事:

  • 非阻塞不会影响客户端和应用程序之间的通信协议:它可以是同步(阻塞)或异步的。非阻塞是一个实现细节;它是应用程序内部的 API 视图。
  • 非阻塞是帮助应用程序实现以下所有功能的实现:可伸缩、反应、响应、弹性、弹性和消息驱动。这意味着它是一个非常重要的设计概念,它存在于许多现代应用的基础上。

众所周知,阻塞 API 和非阻塞 API 不是对立的。它们描述了应用程序的不同方面。阻塞 API 描述了客户端如何与之交互:客户端调用并保持连接,直到提供响应。非阻塞 API 描述了应用程序的实现方式:它不为每个请求指定一个执行线程,而是提供几个异步并发处理的轻量级工作线程。

术语“非阻塞”与为密集输入/输出(I/O)操作提供支持的java.nio(NIO 代表非阻塞输入/输出)包一起使用。

java.io 与 java.nio 包

与内存中的其他进程相比,向外部内存(例如硬盘驱动器)写入和读取数据的操作要慢得多。java.io包中已经存在的类和接口工作得很好,但偶尔会出现性能瓶颈。创建新的java.nio包是为了提供更有效的 I/O 支持。

java.io实现是基于流处理的,正如我们在上一节中看到的,流处理基本上是一个阻塞操作,即使在幕后发生了某种并发。为了提高速度,java.nio实现基于对内存中的缓冲区进行读/写。这样的设计允许我们将填充/清空缓冲区的缓慢过程与从缓冲区快速读取/写入缓冲区的快速过程分开。在某种程度上,它与我们在CompletableFuture类用法示例中所做的类似。在缓冲区中存储数据的另一个优点是,可以检查数据,沿着缓冲区来回移动,这在从流中顺序读取数据时是不可能的。它允许在数据处理过程中具有更大的灵活性。

此外,java.nio实现引入了另一个中间过程,称为通道,它提供缓冲区之间的批量数据传输。读取线程从通道获取数据,并且只接收当前可用的数据,或者什么都不接收(如果通道中没有数据)。如果数据不可用,线程可以执行其他操作,例如向其他通道读/写,而不是保持阻塞状态。同样,在我们的CompletableFuture示例中,当测量系统从传感器读取数据时,主线程可以自由执行任何必须执行的操作。这样,几个工作线程就可以服务于多个 I/O 进程,而不是将一个线程专用于一个 I/O 进程。

这种解决方案被称为非阻塞 I/O,后来应用于其他进程,最突出的是在事件循环中的事件处理,也称为运行循环

事件循环或运行循环

许多非阻塞处理系统都基于事件(或运行)循环——一个持续执行的线程,接收事件(请求、消息),然后将其发送给相应的事件处理程序。事件处理程序没有什么特别之处。它们只是程序员专用于处理特定事件类型的方法(函数)。

此设计称为反应器设计模式,定义为事件处理模式,用于处理并发交付给服务处理程序的服务请求。还提供了对某些事件作出反应并进行相应处理的反应式编程反应式系统的名称。我们稍后将在一个专门的章节中讨论反应式系统。

基于事件循环的设计广泛应用于操作系统和图形用户界面中。它在 Spring5 的 SpringWebFlux 中可用,并在 JavaScript 及其流行的执行环境 Node.js 中实现。最后一个使用事件循环作为其处理主干。Vert.x 工具包也是围绕事件循环构建的。我们将在微服务部分展示后者的一些示例。

在采用事件循环之前,为每个传入请求分配了一个专用线程,这与我们演示的流处理非常相似。每个线程都需要分配一定数量的非特定于请求的资源,因此一些资源(主要是内存分配)被浪费了。然后,随着请求数量的增加,CPU 需要更频繁地将其上下文从一个线程切换到另一个线程,以允许或多或少地并发处理所有请求。在负载下,切换上下文的开销变得足够大,足以影响应用程序的性能。

实现事件循环解决了以下两个问题:

  • 它避免了为每个请求创建一个专用线程,并在处理请求之前一直保留该线程,从而消除了资源浪费。有了事件循环,每个请求都需要更小的内存分配来捕获其细节。它使得在内存中保存更多的请求成为可能,这样它们就可以并发处理。
  • 由于上下文大小的减小,CPU 上下文切换的开销也变得更小。

非阻塞 API 是如何实现请求处理的。有了它,系统能够处理更大的负载(更具伸缩性和弹性),同时保持高度的响应性和弹性。

分布的

随着时间的推移,什么是分布式的概念发生了变化。它过去指的是通过网络连接在多台计算机上运行的应用程序。它甚至有并行计算的同义词,因为应用程序的每个实例都做相同的事情。这样的应用提高了系统的弹性。一台计算机的故障并没有影响整个系统。

然后,又增加了另一个含义:一个应用程序分布在多台计算机上,因此它的每个组件都对整个应用程序产生的结果做出了贡献。这种设计通常用于计算或数据繁重的任务,这些任务需要大量 CPU 电源,或者需要来自许多不同来源的大量数据。

当单个 CPU 强大到足以处理数千台旧计算机和云计算,特别是 AWS Lambda 无服务器计算平台等系统的计算负载时,它从根本上消除了单个计算机的概念;分布式可能指运行在一台或多台计算机上的一个应用程序或其组件的任意组合。

分布式系统的示例包括大数据处理系统、分布式文件或数据存储系统以及账本系统,如区块链或比特币,也可以包括在智能数据存储系统子类别下的数据存储系统组中。

今天,当程序员调用系统分布式时,他们通常指以下内容:

  • 系统可以容忍一个或几个组成部件的故障。
  • 每个系统组件只有一个有限的、不完整的系统视图。
  • 系统的结构是动态的,在执行过程中可能会发生变化。
  • 该系统具有可扩展性。

可伸缩

可伸缩性是指在不显著降低延迟/吞吐量的情况下维持不断增长的负载的能力。传统上,它是通过将软件系统分为几层来实现的:例如,前端层、中间层和后端层。每一层由负责特定处理类型的同一组组件副本的多个部署组成。

前端组件根据从中间层收到的请求和数据负责演示。中间层组件负责基于来自前端层的数据和它们可以从后端层读取的数据进行计算和决策。他们还将数据发送到后端进行存储。后端层存储数据,并将其提供给中间层。

通过添加组件副本,每一层都使我们能够跟上不断增加的负载。在过去,只有在每一层增加更多的计算机才有可能。否则,将没有资源可用于新部署组件的副本。

但是,随着云计算的引入,特别是 AWS Lambda 服务的引入,可伸缩性是通过只添加软件组件的新副本来实现的。部署人员不知道是否有更多的计算机添加到该层。

分布式系统体系结构中的另一个最新趋势使我们能够通过不仅按层扩展,而且按层的特定小功能部分扩展,并提供一种或几种特定类型的服务(称为微服务)来微调可伸缩性。我们将在微服务部分讨论这一点,并展示一些微服务的示例。

有了这样的体系结构,软件系统就变成了许多微服务的组合;每一个都可以根据需要复制多次,以支持所需的处理能力增加。从这个意义上讲,我们可以只在一个微服务级别上讨论可伸缩性。

反应性

术语反应式通常用于反应式编程和反应式系统。反应式编程(也称为 Rx 编程)基于异步数据流(也称为反应流)编程。它是在java.util.concurrent包中用 Java9 引入 Java 的。它允许Publisher生成Subscriber可以异步订阅的数据流。

正如您所看到的,即使没有这个新 API,我们也能够通过使用CompletableFuture异步处理数据。但是,在编写了几次这样的代码之后,人们注意到大部分代码只是管道,因此人们感觉必须有一个更简单、更方便的解决方案。这就是反应流倡议(的方式 http://www.reactive-streams.org 诞生了。工作范围定义如下:

反应流的范围是找到一组最小的接口、方法和协议,这些接口、方法和协议将描述必要的操作和实体,以实现具有非阻塞背压的异步数据流的目标。

术语非阻塞背压指的是异步处理的问题之一,即输入数据的速率与系统处理数据的能力相协调,而无需停止(阻塞)数据输入。解决方案是通知消息源,消费者很难跟上输入,但处理应该以更灵活的方式对传入数据速率的变化作出反应,而不仅仅是阻塞流(因此,名称为 reactive)。

除了标准 Java 库之外,已经存在几个实现反应流 API 的其他库:RxJava、Reactor、Akka Streams 和 Vert.x 是最有名的库。我们将在示例中使用 RXJava2.1.13。您可以在找到 RxJava 2.x APIhttp://reactivex.io ,名称为 ReactiveX,表示被动扩展。

让我们首先比较相同功能的两个实现,使用 RxJava 2.1.13 的java.util.stream包和io.reactivex包,可以通过以下依赖关系添加到项目中:

<dependency>
    <groupId>io.reactivex.rxjava2</groupId>
    <artifactId>rxjava</artifactId>
    <version>2.1.13</version>
</dependency> 

示例程序将非常简单:

  • 创建一个整数流:1,2,3,4,5。
  • 仅过滤偶数(2 和 4)。
  • 计算每个过滤数字的平方根。
  • 计算所有平方根的和。

以下是如何使用java.util.stream包实现:

double a = IntStream.rangeClosed(1, 5)
        .filter(i -> i % 2 == 0)
        .mapToDouble(Double::valueOf)
        .map(Math::sqrt)
        .sum();
System.out.println(a); //prints: 3.414213562373095

使用 RxJava 实现的相同功能如下所示:

Observable.range(1, 5)
        .filter(i -> i % 2 == 0)
        .map(Math::sqrt)
        .reduce((r, d) -> r + d)
        .subscribe(System.out::println); //prints: 3.414213562373095
RxJava is based on the Observable object (which plays the role of Publisher) and Observer that subscribes to the Observable and waits for data to be emitted. 

除了Stream功能外,Observable还有显著不同的功能。例如,流一旦关闭,就不能重新打开,Observable对象可以再次使用。以下是一个例子:

Observable<Double> observable = Observable.range(1, 5)
        .filter(i -> i % 2 == 0)
        .doOnNext(System.out::println)    //prints 2 and 4 twice
        .map(Math::sqrt);
observable
        .reduce((r, d) -> r + d)
        .subscribe(System.out::println);  //prints: 3.414213562373095
observable
        .reduce((r, d) -> r + d)a
        .map(r -> r / 2)
        .subscribe(System.out::println);  //prints: 1.7071067811865475

在前面的示例中,您可以从注释中看到,doOnNext()操作被调用了两次,这意味着observable对象发出了两次值。但如果我们不希望Observable运行两次,我们可以通过添加cache()操作来缓存其数据:

Observable<Double> observable = Observable.range(1,5)
        .filter(i -> i % 2 == 0)
        .doOnNext(System.out::println)  //prints 2 and 4 only once
        .map(Math::sqrt)
        .cache();
observable
        .reduce((r, d) -> r + d)
        .subscribe(System.out::println); //prints: 3.414213562373095
observable
        .reduce((r, d) -> r + d)
        .map(r -> r / 2)
        .subscribe(System.out::println);  //prints: 1.7071067811865475

如您所见,第二次使用相同的Observable利用了缓存的数据,从而实现了更好的性能。Observable接口和 RxJava 中提供了更多的功能,这本书的格式不允许我们描述。但我们希望你能明白。

使用 RxJava 或其他异步流库编写代码构成了反应式编程。它实现了反应性宣言(中宣布的目标 https://www.reactivemanifesto.org 作为构建响应性、弹性、弹性和消息驱动的反应性系统。

反应敏捷的

这个术语似乎是不言自明的。及时响应的能力是每个客户对任何系统的基本要求之一。可以使用许多不同的方法来实现这一点。即使是传统的阻塞 API 也可以得到足够的服务器和其他基础设施的支持,以在非常大的负载下提供预期的响应能力。反应式编程有助于使用更少的硬件。

它是有代价的,因为反应式代码需要改变我们过去的方式,即使是五年前。但过了一段时间,这种新的思维方式变得和任何其他已经熟悉的技能一样自然。在下面的章节中,我们将看到更多的反应式编程示例。

有弹性的

失败是不可避免的。硬件崩溃、软件有缺陷、接收到意外数据或采取了意外且测试不良的执行路径这些事件中的任何一个或它们的组合都可能随时发生。弹性是系统抵御这种情况并继续提供预期结果的能力。

可以通过使用可部署组件和硬件的冗余、系统各部分之间的隔离(从而减少多米诺效应的可能性)、设计系统以便自动更换丢失的部件或发出适当的警报以便有资格的人员进行干预,并通过其他措施。

我们已经讨论了分布式系统。这样的体系结构通过消除单点故障使系统更具弹性。此外,将系统分解为许多使用消息相互通信的专用组件,可以更好地调整最关键部件的复制,并为它们的隔离和潜在故障控制创造更多机会。

有弹力的

维持最大可能负载的能力通常与可伸缩性相关。但在不同载荷下保持相同性能特征的能力称为弹性。弹性系统的客户不应注意空闲周期和峰值负载周期之间的任何差异。

非阻塞反应式实现风格有助于提高这一质量。此外,将程序分解为更小的部分,并将其转换为可以独立部署和管理的服务,这样就可以微调资源分配。这种小型服务称为微服务,其中许多服务可以共同构成一个反应式系统,该系统既可伸缩又具有弹性。我们将在以下章节中更详细地讨论这些解决方案。

消息驱动

我们已经确定,组件的隔离和系统分布是有助于保持系统响应性、弹性和弹性的两个方面。松动和灵活的连接也是支持这些品质的重要条件。而反应式系统的异步本质并没有让设计者有任何其他选择,而是在消息上构建组件之间的通信。

它在每个组件周围创造了一个呼吸空间,没有这个呼吸空间,系统将是一个紧密耦合的整体,容易出现各种问题,更不用说维护噩梦了。

在此基础上,我们将研究可用于将应用程序构建为提供所需业务功能的松散耦合服务集合的体系结构样式。

微服务

为了将可部署的代码单元鉴定为微服务,它必须具备以下特征:

  • 一个微服务的源代码大小应该小于传统应用程序的大小。另一个规模标准是一个程序员团队应该能够编写和支持其中的几个。
  • 它必须独立部署。当然,一个微服务通常会与其他系统合作,并期望得到其他系统的合作,但这不应妨碍我们部署它的能力。
  • 如果微服务使用数据库存储数据,它必须有自己的模式或一组表。这种说法仍在争论中,特别是在多个服务修改同一数据集或相互依赖的数据集的情况下。如果同一个团队拥有所有相关服务,则更容易完成。否则,有几种可能的策略来确保独立的微服务开发和部署。
  • 它必须是无状态的,即它的状态不应该保存在内存中,除非内存是共享的。如果服务的一个实例失败,那么另一个实例应该能够完成服务的预期任务。
  • 它应该提供一种方法来检查其运行状况——服务是否已启动并正在运行,是否已准备好执行该任务。

也就是说,让我们来看一下微服务实现的工具包领域。一个人完全可以从头开始编写微服务,但在编写之前,一定要看看已经存在的内容,即使您发现没有任何内容适合您的特定需求。

最流行的两个工具包是 Spring Boot(https://projects.spring.io/spring-boot 和原始 J2EE。J2EE 社区创建了微文件(https://microprofile.io 倡议,其公开目标是为微服务体系结构优化企业 Java。库穆鲁泽(https://ee.kumuluz.com 是一个轻量级开源微服务框架,与 MicroFile 兼容。

其他一些框架、库和工具包的列表包括以下内容(按字母顺序排列):

列出的所有框架、库和工具包都支持微服务之间的 HTTP/JSON 通信。他们中的一些人还有另外一种发送信息的方式。如果没有,则可以使用任何轻量级消息传递系统。我们在这里提到,因为,如您所记得的,消息驱动异步处理是由微服务组成的反应系统的弹性、响应性和弹性的基础。

为了演示构建微服务的过程,我们将使用 Vert.x,这是一个事件驱动的非阻塞轻量级多语言工具包(组件可以用 Java、JavaScript、Groovy、Ruby、Scala、Kotlin 或 Ceylon 编写)。它支持一个异步编程模型和一个分布式事件总线,可以访问浏览器中的 JavaScript,允许创建实时 web 应用程序。

Vert.x 基础

Vert.x world 中的构建块是一个实现io.vertx.core.Verticle接口的类:

package io.vertx.core;
public interface Verticle {
  Vertx getVertx();
  void init(Vertx vertx, Context context);
  void start(Future<Void> future) throws Exception;
  void stop(Future<Void> future) throws Exception;
}

前面接口的实现称为 verticle。前面接口的大多数方法名称都是自解释的。getVertex()方法提供对Vertx对象的访问,该对象是 Vert.x Core API 的入口点,该 API 的方法允许我们构建微服务构建所需的以下功能:

  • 创建 DNS 客户端
  • 创建定期服务
  • 创建数据报套接字
  • 部署和取消部署垂直线
  • 提供对共享数据 API 的访问
  • 创建 TCP 和 HTTP 客户端和服务器
  • 提供对事件总线和文件系统的访问

所有部署的垂直站点都可以通过标准 HTTP 协议或使用io.vertx.core.eventbus.EventBus相互通信,形成一个微服务系统。我们将展示如何使用io.vertx.rxjava包中的 verticles 和 RxJava 实现构建反应式微服务系统。

通过扩展io.vertx.rxjava.core.AbstractVerticle类,可以轻松创建Verticle接口实现:

package io.vertx.rxjava.core;
import io.vertx.core.Vertx;
import io.vertx.core.Context;
import io.vertx.core.AbstractVerticle
public class AbstractVerticle extends AbstractVerticle {
   protected io.vertx.rxjava.core.Vertx vertx;
   public void init(Vertx vertx, Context context) {
      super.init(vertx, context);
      this.vertx = new io.vertx.rxjava.core.Vertx(vertx);
   } 
}

如您所见,前面的类扩展了io.vertx.core.AbstractVerticle类:

package io.vertx.core;
import java.util.List;
import io.vertx.core.Verticle;
import io.vertx.core.json.JsonObject;
public abstract class AbstractVerticle implements Verticle {
   protected Vertx vertx;
   protected Context context;
   public void init(Vertx vertx, Context context) {
      this.vertx = vertx;
      this.context = context;
   }
   public Vertx getVertx() { return vertx; }
   public JsonObject config() { return context.config(); }
   public String deploymentID() { return context.deploymentID(); }
   public List<String> processArgs() { return context.processArgs(); }
   public void start(Future<Void> startFuture) throws Exception {
      start();
      startFuture.complete();
   }
   public void stop(Future<Void> stopFuture) throws Exception {
      stop();
      stopFuture.complete();
   }
   public void start() throws Exception {}
   public void stop() throws Exception {}
}

如您所见,您所需要做的就是扩展io.vertx.rxjava.core.AbstractVerticle类并实现start()方法。新的垂直体将是可部署的,即使没有实现start()方法,但它不会做任何有用的事情。start()方法中的代码是应用程序功能的入口点。

要使用 Vert.x 并执行示例,必须将以下依赖项添加到项目中:

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-web</artifactId>
    <version>${vertx.version}</version>
</dependency>
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-rx-java</artifactId>
    <version>${vertx.version}</version>
</dependency>

vertx.version属性可以在pom.xml文件的properties部分设置:

<properties>
    <vertx.version>3.5.1</vertx.version>
</properties>

使 verticle 具有反应性的是事件循环(线程)的底层实现,它接收事件(请求)并将其传递给处理程序—verticle 中的方法或另一个处理此类事件的专用类。程序员通常将它们描述为与每个事件类型关联的函数。当处理程序返回时,事件循环调用回调,实现我们在上一节中讨论的反应器模式。

对于自然阻塞的某些类型的过程(例如 JDBC 调用或长时间计算),可以异步执行辅助垂直循环,而不是通过事件循环(因此,不阻塞它),而是通过单独的线程,使用vertx.executeBlocking()方法。基于事件循环的应用程序设计的黄金法则是,*不要阻塞事件循环!*违反此规则会使应用程序停止运行。

作为微服务的 HTTP 服务器

例如,下面是一个充当 HTTP 服务器的垂直体:

package com.packt.javapath.ch18demo.microservices;
import io.vertx.rxjava.core.AbstractVerticle;
import io.vertx.rxjava.core.http.HttpServer;
public class HttpServer1 extends AbstractVerticle{
   private int port;
   public HttpServer1(int port) {
       this.port = port;
   }
   public void start() throws Exception {
      HttpServer server = vertx.createHttpServer();
      server.requestStream().toObservable()
         .subscribe(request -> request.response()
             .end("Hello from " + Thread.currentThread().getName() + 
                                         " on port " + port + "!\n\n"));
      server.rxListen(port).subscribe();
      System.out.println(Thread.currentThread().getName() + 
                                 " is waiting on port " + port + "...");
   }
}

在前面的代码中,服务器被创建,来自可能请求的数据流被包装到一个Observable中。Observable发出的数据被传递到处理请求并生成必要响应的函数(请求处理程序)。我们还告诉服务器要侦听哪个端口,现在可以部署此垂直通道的多个实例来侦听不同的端口:

vertx().getDelegate().deployVerticle(new HttpServer1(8082));
vertx().getDelegate().deployVerticle(new HttpServer1(8083));

还有一个io.vertx.rxjava.core.RxHelper助手类可用于部署。它关注一些对当前讨论不重要的细节:

RxHelper.deployVerticle(vertx(), new HttpServer1(8082));
RxHelper.deployVerticle(vertx(), new HttpServer1(8083));

无论使用哪种方法,您都将看到以下消息:

vert.x-eventloop-thread-0 is waiting on port 8082...
vert.x-eventloop-thread-0 is waiting on port 8083...

这些消息证实了我们的预期:相同的事件循环线程正在两个端口上侦听。现在,我们可以使用标准的curl命令向任何正在运行的服务器发出请求,例如:

curl localhost:8082

响应将是我们硬编码的:

Hello from vert.x-eventloop-thread-0 on port 8082!

作为微服务的定期服务

x 还允许我们创建一个定期服务,它以固定的时间间隔做一些事情。以下是一个例子:

package com.packt.javapath.ch18demo.microservices;
import io.vertx.rxjava.core.AbstractVerticle;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
public class PeriodicService1 extends AbstractVerticle {
  public void start() throws Exception {
     LocalTime start = LocalTime.now();
     vertx.setPeriodic(1000, v-> {
         System.out.println("Beep!");
         if(ChronoUnit.SECONDS.between(start, LocalTime.now()) > 3 ){
             vertx.undeploy(deploymentID());
         }
     });
     System.out.println("Vertical PeriodicService1 is deployed");
  }
  public void stop() throws Exception {
     System.out.println("Vertical PeriodicService1 is un-deployed");
  }
}

正如您所看到的,这个垂直的,一旦部署,每秒打印一次Beep!消息,三秒钟后,就会自动取消部署。如果我们部署此垂直,我们将看到:

Vertical PeriodicService1 is deployed
Beep!
Beep!
Beep!
Beep!
Vertical PeriodicService1 is un-deployed

第一个Beep!在垂直开始时出现,然后每秒会有三条以上的消息,并且垂直未部署,正如预期的那样。

作为微服务的 HTTP 客户端

我们可以使用定期垂直服务来使用 HTTP 协议向垂直服务器发送消息。为了做到这一点,我们需要一个新的依赖项,因此我们可以使用WebClient类:

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-web-client</artifactId>
    <version>${vertx.version}</version>
</dependency>

这样,向 HTTP 服务器发送消息的定期服务如下所示:

package com.packt.javapath.ch18demo.microservices;
import io.vertx.rxjava.core.AbstractVerticle;
import io.vertx.rxjava.core.buffer.Buffer;
import io.vertx.rxjava.ext.web.client.HttpResponse;
import io.vertx.rxjava.ext.web.client.WebClient;
import rx.Single;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
public class PeriodicService2 extends AbstractVerticle {
    private int port;
    public PeriodicService2(int port) {
        this.port = port;
    }
    public void start() throws Exception {
        WebClient client = WebClient.create(vertx);
        Single<HttpResponse<Buffer>> single = client
                .get(port, "localhost", "?name=Nick")
                .rxSend();
        LocalTime start = LocalTime.now();
        vertx.setPeriodic(1000, v-> {
           single.subscribe(r-> System.out.println(r.bodyAsString()),
                             Throwable::printStackTrace);
           if(ChronoUnit.SECONDS.between(start, LocalTime.now()) >= 3 ){
              client.close(); 
              vertx.undeploy(deploymentID());
              System.out.println("Vertical PeriodicService2 undeployed");
           }
        });
        System.out.println("Vertical PeriodicService2 deployed");
    }
}

如您所见,此定期服务接受端口号作为其构造函数的参数,然后每秒向本地主机上的此端口发送一条消息,并在三秒后自行取消部署。该消息是name参数的值。默认情况下,它是 GET 请求。

我们还将修改服务器垂直,以读取name参数的值:

public void start() throws Exception {
    HttpServer server = vertx.createHttpServer();
    server.requestStream().toObservable()
          .subscribe(request -> request.response()
             .end("Hi, " + request.getParam("name") + "! Hello from " + 
          Thread.currentThread().getName() + " on port " + port + "!"));
    server.rxListen(port).subscribe();
    System.out.println(Thread.currentThread().getName()
                               + " is waiting on port " + port + "...");
}

我们可以部署两个垂直领域:

RxHelper.deployVerticle(vertx(), new HttpServer2(8082));
RxHelper.deployVerticle(vertx(), new PeriodicService2(8082));

输出结果如下:

Vertical PeriodicService2 deployed
vert.x-eventloop-thread-0 is waiting on port 8082...
Hi, Nick! Hello from vert.x-eventloop-thread-0 on port 8082!
Hi, Nick! Hello from vert.x-eventloop-thread-0 on port 8082!
Vertical PeriodicService2 undeployed
Hi, Nick! Hello from vert.x-eventloop-thread-0 on port 8082!

其他微服务

原则上,整个微服务系统可以基于使用 HTTP 协议发送的消息来构建,每个微服务都实现为 HTTP 服务器或 HTTP 服务器作为消息交换的前端。或者,任何其他消息传递系统可用于通信。

对于 Vert.x,它有自己的基于事件总线的消息传递系统。在下一节中,我们将对其进行演示,并将其用作反应式系统外观的说明。

我们的示例微服务的大小可能会给人留下这样的印象:微服务必须像对象方法一样细粒度。例如,在某些情况下,值得考虑是否需要缩放特定方法。事实上,这种体系结构风格足够新颖,可以提供确定大小的建议,而现有的框架、库和工具包足够灵活,可以支持几乎任何大小的独立部署服务。那么,如果一个可部署的独立服务与传统应用程序一样大,那么它可能不会被称为微服务,而是一个外部系统或类似的东西。

反应系统

熟悉事件驱动架构EDA的人可能已经注意到,它与反应式系统的概念非常相似。他们的描述使用非常相似的语言和图表。不同之处在于 EDA 只处理软件系统的一个方面——体系结构。另一方面,反应式系统的概念更多地是关于代码样式和执行流,例如,强调使用异步数据流。因此,反应式系统可以有 EDA,EDA 可以作为反应式系统来实现。

让我们看看另一组示例,这些示例提供了一个反应式系统的外观,如果使用 Vert.x 实现的话。请注意,Vert.xapi 有两个源代码树:一个以io.vertx.core开头,另一个以io.vertx.rxjava开头。因为我们正在讨论反应式编程,所以我们将使用io.vertx.rxjava下的包,称为 rx fied Vert.xapi。

消息驱动系统

Vert.x 有一个直接支持消息驱动体系结构和 EDA 的特性。它被称为事件总线。任何 verticle 都可以访问事件总线,并且可以使用io.vertx.core.eventbus.EventBus类或其表亲io.vertx.rxjava.core.eventbus.EventBus将任何消息发送到任何地址(只是一个字符串)。我们只打算使用后者,但在io.vertx.core.eventbus.EventBus中也有类似的功能(不是 rx-fied)。一个或多个垂直链接可以注册为特定地址的消息使用者。如果同一地址有多个 Verticle 用户,则EventBusrxSend()方法仅向其中一个用户发送消息,使用循环算法选择下一条消息的接收者。或者,正如您所期望的,publish()方法将消息传递给具有相同地址的所有消费者。以下是将消息发送到指定地址的代码:

vertx.eventBus().rxSend(address, msg).subscribe(reply -> 
    System.out.println("Got reply: " + reply.body()), 
    Throwable::printStackTrace );

rxSend()方法返回表示可以接收的消息的Single<Message>对象,subscribe()方法返回给。。。好订阅它。Single<Message>类实现了单值响应的反应模式。subscribe()方法接受两个Consumer功能:第一个处理回复,第二个处理错误。在前面的代码中,第一个函数仅打印回复:

reply -> System.out.println("Got reply: " + reply.body())

第二个操作打印异常的堆栈跟踪(如果发生):

Throwable::printStackTrace

如您所知,前面的构造称为方法引用。与 lambda 表达式相同的函数如下所示:

e -> e.printStackTrace()

publish()方法的调用看起来类似:

vertx.eventBus().publish(address, msg)

它可能会将消息发布给许多使用者,因此该方法不会返回Single对象或任何其他可用于获取回复的对象。相反,它只返回一个EventBus对象;如果需要,可以调用更多的事件总线方法。

消息消费者

Vert.x 中的消息使用者是一个垂直体,它在事件总线中注册为发送或发布到指定地址的消息的潜在接收者:

package com.packt.javapath.ch18demo.reactivesystem;
import io.vertx.rxjava.core.AbstractVerticle;
public class MsgConsumer extends AbstractVerticle {
    private String address, name;
    public MsgConsumer(String id, String address) {
        this.address = address;
        this.name = this.getClass().getSimpleName() + 
                                    "(" + id + "," + address + ")";
    }
    public void start() throws Exception {
        System.out.println(name + " starts...");
        vertx.eventBus().consumer(address).toObservable()
         .subscribe(msg -> {
            String reply = name + " got message: " + msg.body();
            System.out.println(reply);
            if ("undeploy".equals(msg.body())) {
                vertx.undeploy(deploymentID());
                reply = name + " undeployed.";
                System.out.println(reply);
            }
            msg.reply(reply);
        }, Throwable::printStackTrace );
        System.out.println(Thread.currentThread().getName()
                + " is waiting on address " + address + "...");
    }
}

consumer(address)方法返回一个io.vertx.rxjava.core.eventbus.MessageConsumer<T>对象,该对象表示到所提供地址的消息流。这意味着可以将流转换为Observable并订阅它以接收发送到此地址的所有消息。Observable对象的subscribe()方法接受两个Consumer功能:第一个处理接收到的消息,第二个在发生错误时执行。在第一个函数中,我们包含了msg.reply(reply)方法,它将消息发送回消息源。您可能还记得,如果原始消息是通过rxSend()方法发送的,那么发送者就能够获得此回复。如果改用了publish()方法,那么msg.reply(reply)方法发送的回复将毫无用处。

另外,请注意,当接收到undeploy消息时,消息使用者将取消部署自身。此方法通常仅在自动部署期间使用,即在不关闭系统的情况下,将旧版本替换为新版本。

因为我们将部署多个具有相同地址的消息使用者进行演示,所以我们添加了id参数并将其包含在name值中。该值作为所有消息的前缀,因此我们可以跟踪消息在整个系统中的传播方式。

您可能已经意识到前面的实现只是一个 shell,可以用来调用一些有用的功能。接收到的消息可以是执行某些操作的命令、要处理的数据、要存储在数据库中的数据或任何其他内容。回复可以是消息已收到的确认,也可以是其他预期结果。如果是后者,则处理应该非常快,以避免阻塞事件循环(记住黄金法则)。如果无法快速完成处理,则重播也可以是回调令牌,发送方稍后使用它来检索结果。

消息发送者

我们将演示的消息发送器基于我们在微服务部分中演示的 HTTP 服务器实现。没有必要这样做。在实际代码中,垂直线通常会自动发送消息,以获取所需数据、提供其他垂直线需要的数据、通知其他垂直线、将数据存储在数据库中或出于任何其他原因。但出于演示目的,我们决定发送方将监听某个端口的消息,我们将手动(使用curl命令)或通过微服务部分所述的定期服务自动向其发送消息。这就是为什么消息发送者看起来比消息使用者更复杂的原因:

package com.packt.javapath.ch18demo.reactivesystem;
import io.vertx.rxjava.core.AbstractVerticle;
import io.vertx.rxjava.core.http.HttpServer;
public class EventBusSend extends AbstractVerticle {
    private int port;
    private String address, name;
    public EventBusSend(int port, String address) {
       this.port = port;
       this.address = address;
       this.name = this.getClass().getSimpleName() + 
                      "(port " + port + ", send to " + address + ")";
    }
    public void start() throws Exception {
       System.out.println(name + " starts...");
       HttpServer server = vertx.createHttpServer();
       server.requestStream().toObservable().subscribe(request -> {
         String msg = request.getParam("msg");
         request.response().setStatusCode(200).end();
 vertx.eventBus().rxSend(address, msg).subscribe(reply -> {
            System.out.println(name + " got reply:\n  " + reply.body());
         },
         e -> {
            if(StringUtils.contains(e.toString(), "NO_HANDLERS")){
                vertx.undeploy(deploymentID());
                System.out.println(name + " undeployed.");
            } else {
                e.printStackTrace();
            }
         }); });
       server.rxListen(port).subscribe();
       System.out.println(Thread.currentThread().getName()
                               + " is waiting on port " + port + "...");
    }
}

前面的大部分代码都与 HTTP 服务器功能相关。发送消息(由 HTTP 服务器接收)的几行代码如下:

        vertx.eventBus().rxSend(address, msg).subscribe(reply -> {
            System.out.println(name + " got reply:\n  " + reply.body());
        }, e -> {
            if(StringUtils.contains(e.toString(), "NO_HANDLERS")){
                vertx.undeploy(deploymentID());
                System.out.println(name + " undeployed.");
            } else {
                e.printStackTrace();
            }
        });

消息发送后,发送者订阅可能的回复并打印它(如果收到回复)。如果发生错误(发送消息时抛出异常),我们可以检查异常(转换为String值)是否包含文字NO_HANDLERS,如果是,则取消部署发送方。我们花了一段时间才弄明白,当没有消费者被分配到该地址时,如何识别这种情况,而该发送者专门为该地址发送消息。如果没有使用者(很可能所有使用者都未部署),则不需要发送者,因此我们将取消部署它。

清理和取消部署所有不再需要的垂直通道是一个很好的做法。但是如果您在 IDE 中运行 Verticle,那么很可能在您停止 IDE 中的主进程(创建 Verticle 的主进程)后,所有 Verticle 都会立即停止。如果没有,运行jcmd命令,查看是否仍有 Vert.x 垂直轴在运行。列出的每个进程的第一个数字是进程 ID。确定不再需要的垂直点,并使用kill -9 <process ID>命令停止它们。

现在,让我们部署两个消息使用者,并通过我们的消息发送者向他们发送消息:

String address = "One";
Vertx vertx = vertx();
RxHelper.deployVerticle(vertx, new MsgConsumer("1",address));
RxHelper.deployVerticle(vertx, new MsgConsumer("2",address));
RxHelper.deployVerticle(vertx, new EventBusSend(8082, address));

运行上述代码后,终端将显示以下消息:

MsgConsumer(1,One) starts...
MsgConsumer(2,One) starts...
EventBusSend(port 8082, send to One) starts...
vert.x-eventloop-thread-1 is waiting on address One...
vert.x-eventloop-thread-0 is waiting on address One...
vert.x-eventloop-thread-2 is waiting on port 8082...

请注意,运行不同的事件循环来支持每个垂直。

现在,让我们从终端窗口使用以下命令发送一些消息:

curl localhost:8082?msg=Hello!
curl localhost:8082?msg=Hi!
curl localhost:8082?msg=How+are+you?
curl localhost:8082?msg=Just+saying...

加号(+)是必要的,因为 URL 不能包含空格,必须用编码为,这意味着,除其他外,用加号+%20替换空格。响应前面的命令,我们将看到以下消息:

MsgConsumer(2,One) got message: Hello!
EventBusSend(port 8082, send to One) got reply:
 MsgConsumer(2,One) got message: Hello!
MsgConsumer(1,One) got message: Hi!
EventBusSend(port 8082, send to One) got reply:
 MsgConsumer(1,One) got message: Hi!
MsgConsumer(2,One) got message: How are you?
EventBusSend(port 8082, send to One) got reply:
 MsgConsumer(2,One) got message: How are you?
MsgConsumer(1,One) got message: Just saying...
EventBusSend(port 8082, send to One) got reply:
 MsgConsumer(1,One) got message: Just saying...

正如预期的那样,根据循环算法,消费者轮流接收消息。现在,让我们部署所有垂直线:

curl localhost:8082?msg=undeploy
curl localhost:8082?msg=undeploy
curl localhost:8082?msg=undeploy

以下是响应上述命令显示的消息:

MsgConsumer(1,One) got message: undeploy
MsgConsumer(1,One) undeployed.
EventBusSend(port 8082, send to One) got reply:
 MsgConsumer(1,One) undeployed.
MsgConsumer(2,One) got message: undeploy
MsgConsumer(2,One) undeployed.
EventBusSend(port 8082, send to One) got reply:
 MsgConsumer(2,One) undeployed.
EventBusSend(port 8082, send to One) undeployed.

根据前面的消息,我们所有的垂直平台都未部署。如果我们再次提交undeploy消息,我们将看到:

curl localhost:8082?msg=undeploy
curl: (7) Failed to connect to localhost port 8082: Connection refused

这是因为发送方未部署,并且没有侦听本地主机端口8082的 HTTP 服务器。

消息发布者

我们实现的消息发布者与消息发送者非常相似:

package com.packt.javapath.ch18demo.reactivesystem;

import io.vertx.rxjava.core.AbstractVerticle;
import io.vertx.rxjava.core.http.HttpServer;

public class EventBusPublish extends AbstractVerticle {
    private int port;
    private String address, name;
    public EventBusPublish(int port, String address) {
        this.port = port;
        this.address = address;
        this.name = this.getClass().getSimpleName() + 
                    "(port " + port + ", publish to " + address + ")";
    }
    public void start() throws Exception {
        System.out.println(name + " starts...");
        HttpServer server = vertx.createHttpServer();
        server.requestStream().toObservable()
                .subscribe(request -> {
                    String msg = request.getParam("msg");
                    request.response().setStatusCode(200).end();
 vertx.eventBus().publish(address, msg);
                    if ("undeploy".equals(msg)) {
 vertx.undeploy(deploymentID());
                        System.out.println(name + " undeployed.");
                    }
                });
        server.rxListen(port).subscribe();
        System.out.println(Thread.currentThread().getName()
                + " is waiting on port " + port + "...");
    }
}

发布者与发送者的区别仅在于此部分:

            vertx.eventBus().publish(address, msg);
            if ("undeploy".equals(msg)) {
                vertx.undeploy(deploymentID());
                System.out.println(name + " undeployed.");
            }

由于发布时无法获得回复,因此前面的代码比消息发送代码简单得多。此外,由于所有使用者同时接收到undeploy消息,我们可以假设它们都将被取消部署,发布者可以自行取消部署。让我们通过运行以下程序来测试它:

String address = "One";
Vertx vertx = vertx();
RxHelper.deployVerticle(vertx, new MsgConsumer("1",address));
RxHelper.deployVerticle(vertx, new MsgConsumer("2",address));
RxHelper.deployVerticle(vertx, new EventBusPublish(8082, address));

为了响应前面的代码执行,我们得到以下消息:

MsgConsumer(1,One) starts...
MsgConsumer(2,One) starts...
EventBusPublish(port 8082, publish to One) starts...
vert.x-eventloop-thread-2 is waiting on port 8082...

现在,我们在另一个终端窗口中发出以下命令:

curl localhost:8082?msg=Hello!

垂直运行的终端窗口中的消息如下所示:

MsgConsumer(1,One) got message: Hello!
MsgConsumer(2,One) got message: Hello!

正如预期的那样,具有相同地址的两个消费者接收相同的消息。现在,让我们取消部署它们:

curl localhost:8082?msg=undeploy

垂直方向以以下消息响应:

MsgConsumer(1,One) got message: undeploy
MsgConsumer(2,One) got message: undeploy
EventBusPublish(port 8082, publish to One) undeployed.
MsgConsumer(1,One) undeployed.
MsgConsumer(2,One) undeployed.

如果我们再次提交undeploy消息,我们将看到:

curl localhost:8082?msg=undeploy
curl: (7) Failed to connect to localhost port 8082: Connection refused

至此,我们已经完成了由微服务组成的反应式系统的演示。添加一些有用的方法和类将使它更接近实际系统。但我们将留给读者作为练习。

现实检查

我们已经在一个 JVM 进程中运行了前面的所有示例。如果需要,Vert.x 实例可以部署在不同的 JVM 进程中,并通过向run命令添加-cluster选项进行集群,此时垂直实例不是从 IDE 而是从命令行部署的。集群垂直共享事件总线,所有 Vert.x 实例都可以看到这些地址。这样,如果某些地址的使用者不能及时处理请求(消息),则可以部署更多的消息使用者。

我们前面提到的其他框架也有类似的功能。它们使微服务的创建变得容易,并可能鼓励将应用程序分解为微小的、单一方法的操作,以期组装一个非常有弹性和响应性的系统。然而,这些并不是优秀软件的唯一标准。系统分解增加了其部署的复杂性。此外,如果一个开发团队负责许多微服务,那么在不同阶段(开发、测试、集成测试、认证、登台和生产)对如此多的部分进行版本控制的复杂性可能会导致混乱。部署过程可能变得非常复杂,因此有必要放慢更改速度,以使系统与市场需求保持同步。

除了开发微服务,还必须解决许多其他方面,以支持反应式系统:

  • 必须建立一个监控系统来提供对应用程序状态的洞察,但是它的开发不应该太复杂,以至于将开发资源从主应用程序中抽离。
  • 必须安装警报,以便及时向团队警告可能的和实际的问题,以便在影响业务之前解决这些问题。
  • 如有可能,必须实施自动纠正流程。例如,必须实现重试逻辑,在声明失败之前,具有合理的尝试上限。
  • 当一个组件的故障剥夺了其他组件必要的资源时,一层断路器必须保护系统免受多米诺效应的影响。
  • 嵌入式测试系统应该能够引入中断并模拟负载增加,以确保应用程序的弹性和响应性不会随着时间的推移而降低。例如,Netflix 团队引入了一个混沌猴子——一个能够关闭生产系统各个部分并测试其恢复能力的系统。他们甚至在生产中也使用它,因为生产环境具有特定的配置,而在另一个环境中进行的测试不能保证找到所有可能的问题。

正如您现在可能已经意识到的那样,在承诺使用反应式系统之前,团队必须权衡所有的利弊,以准确理解他们为什么需要反应式系统,以及其开发的价格。古老的格言是没有价值可以免费增加。无功系统的强大威力伴随着复杂性的相应增长,不仅在开发过程中,而且在系统调整和维护过程中。

然而,如果传统的系统无法解决您所面临的处理问题,或者如果您对所有反应式的事物都充满热情,并且喜欢这个概念,那么请尽一切努力。这趟旅程将充满挑战,但回报是值得的。正如另一句古老的格言所说,容易实现的事情不值得付出努力

练习–创建 io.reactivex.Observable

编写代码,演示创建io.reactivex.Observable的几种方法。在每个示例中,订阅创建的Observable对象并打印发出的值。

我们没有讨论这个问题,所以您需要学习 RxJava2API 并在 internet 上查找示例。

答复

以下是允许您创建io.reactivex.Observable的六种方法:

//1
Observable.just("Hi!").subscribe(System.out::println); //prints: Hi!
//2
Observable.fromIterable(List.of("1","2","3"))
          .subscribe(System.out::print); //prints: 123
System.out.println();
//3
String[] arr = {"1","2","3"};
Observable.fromArray(arr).subscribe(System.out::print); //prints: 123
System.out.println();
//4
Observable.fromCallable(()->123)
          .subscribe(System.out::println); //prints: 123
//5
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<String> future = pool
        .submit(() -> {
            Thread.sleep(100);
            return "Hi!";
        });
Observable.fromFuture(future)
          .subscribe(System.out::println); //prints: Hi!
pool.shutdown();
//6
Observable.interval(100, TimeUnit.MILLISECONDS)
          .subscribe(v->System.out.println("100 ms is over")); 
                                     //prints twice "100 ms is over"
try { //this pause gives the above method a chance to print the message
    TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
    e.printStackTrace();
}

总结

在本书的最后一章中,我们向读者简要介绍了现实生活中的专业编程以及行业挑战。我们回顾了与大数据处理相关的许多现代术语,这些术语使用高度可扩展的响应式和弹性反应式系统,能够解决现代具有挑战性的处理问题。我们甚至提供了此类系统的代码示例,这可能是您实际项目的第一步。

我们希望你们保持好奇心,继续学习和实验,最终建立一个系统,解决一个真正的问题,给世界带来更多的幸福。