Spring微服务实战第8章 使用Spring Cloud Stream的事件驱动架构

本章介绍了使用Spring Cloud Stream实现微服务间基于消息传递的事件驱动架构,以解决服务间的耦合、脆弱性和扩展性问题。通过对比同步请求-响应模型和消息传递模型,阐述了消息传递的优势,如松耦合、耐久性、可伸缩性和灵活性。详细讲解了Spring Cloud Stream的组件和工作原理,并提供了在许可证服务和组织服务中使用Spring Cloud Stream构建消息生产者和消费者的示例。最后,展示了如何利用Spring Cloud Stream结合Redis实现分布式缓存,以及如何定义自定义通道和处理消息事件。
摘要由CSDN通过智能技术生成

第8章 使用Spring Cloud Stream的事件驱动架构

本章主要内容

  • 了解事件驱动的架构处理以及它与微服务的相关性
  • 使用Spring Cloud Stream简化微服务中的事件处理
  • 配置Spring Cloud Stream
  • 使用Spring Cloud Stream和Kafka发布消息
  • 使用Spring Cloud Stream和Kafka消费消息
  • 使用Spring Cloud Stream、Kafka和Redis实现分布式缓存

还记得最后一次和别人坐下来聊天是什么时候吗?回想一下你是如何与那个人进行互动的。你完全专注于信息交换(就是在你说完之后,在等待对方完全回复之前什么都没有做)吗?当你说话的时候,你完全专注于谈话,而不让外界的东西分散自己的注意力吗?如果这场谈话中有两位以上的参与者,你重复了你对每位对话参与者所说的话,然后依次等待他们的回应吗?如果你对上述问题的回答都是“是”,那就说明你已经得道开悟,超越了我等凡人,那么你应该停止你正在做的事情,因为你现在可以回答这个古老的问题:“一只手鼓掌的声音是什么?”另外,我猜你没有孩子。

事实上,人类总是处于一种运动状态,与周围的环境相互作用,同时发送信息给周围的事物并接收信息。在我家里,一个典型的对话可能是这样的:在和老婆说话的时候我正忙着洗碗,我正在向她描述我的一天,此时,她正玩着她的手机,并聆听着、处理着我说的话,然后偶尔给予回应。当我在洗碗的时候,我听到隔壁房间里有一阵骚动。我停下手头的事情,冲进隔壁房间去看看出了什么问题,然后我就看到我们那只9个月大的小狗维德咬住了我3岁大的儿子的鞋,像拿着战利品般在客厅里到处跑,而我3岁的儿子对此情此景感到不满。我满屋子追狗,直到把鞋子拿回来。然后我回去洗碗,继续和我的老婆聊天。

我跟大家说这件事并不是想告诉大家我生活中普通的一天,而是想要指出我们与世界的互动不是同步的、线性的,不能狭义地定义为一个请求-响应模型。它是消息驱动的,在这里,我们不断地发送和接收消息。当我们收到消息时,我们会对这些消息作出反应,同时经常打断我们正在处理的主要任务。

本章将介绍如何设计和实现基于Spring的微服务,以便与其他使用异步消息的微服务进行通信。使用异步消息在应用程序之间进行通信并不新鲜,新鲜的是使用消息实现事件通信的概念,这些事件代表了状态的变化。这个概念称为事件驱动架构(Event Driven Architecture,EDA),也被称为消息驱动架构(Message Driven Architecture,MDA)。基于EDA的方法允许开发人员构建高度解耦的系统,它可以对变更作出反应,而不需要与特定的库或服务紧密耦合。当与微服务结合后,EDA通过仅让服务监听由应用程序发出的事件流(消息)的方式,允许开发人员迅速地向应用程序中添加新功能。

Spring Cloud项目通过Spring Cloud Stream子项目使构建基于消息传递的解决方案变得轻而易举。Spring Cloud Stream允许开发人员轻松实现消息发布和消费,同时屏蔽与底层消息传递平台相关的实现细节。

8.1 为什么使用消息传递、EDA和微服务

为什么消息传递在构建基于微服务的应用程序中很重要?为了回答这个问题,让我们从一个例子开始。本章将使用贯穿全书的两项服务:许可证服务和组织服务。让我们想象一下,将这些服务部署到生产环境之后,我们会发现,从组织服务中查找组织信息时,许可证服务调用花费了非常长的时间。在查看组织数据的使用模式时,我们会发现组织数据很少会更改,并且组织服务中读取的大多数数据都是按照组织记录的主键完成的。如果可以为组织数据缓存读操作从而节省访问数据库的成本,那么就可以极大地改善许可证服务调用的响应时间。

在实施缓存解决方案时,我们会意识到有以下3个核心要求。

(1)缓存的数据需要在许可证服务的所有实例之间保持一致——这意味着不能在许可证服务本地中缓存数据,因为要保证无论服务实例如何都能读取相同的组织数据。

(2)不能将组织数据缓存在托管许可证服务的容器的内存中——托管服务的运行时容器通常受到大小限制,并且可以使用不同的访问模式来对数据进行访问。本地缓存可能会带来复杂性,因为必须保证本地缓存与集群中的所有其他服务同步。

(3)在更新或删除一个组织记录时,开发人员希望许可证服务能够识别出组织服务中出现了状态更改——许可证服务应该使该组织的所有缓存数据失效,并将它从缓存中删除。

我们来看看实现这些要求的两种方法。第一种方法将使用同步请求-响应模型来实现上述要求。在组织状态发生变化时,许可证服务和组织服务通过它们的REST端点进行通信。第二种方法是组织服务发出异步事件(消息),该事件将通报组织服务数据已经发生了变化。使用第二种方法,组织服务将发布一条组织记录已被更新或删除的消息到队列。许可证服务将监听中介,了解到一个组织事件已发生,并清除其缓存中的组织数据。

8.1.1 使用同步请求-响应方式来传达状态变化

对于组织数据缓存,我们将使用分布式的键值存储数据库Redis。图8-1提供了一个高层次概览,讲述如何使用传统的同步请求-响应编程模型构建高速缓存解决方案。

image-20210910142904244

图8-1 在同步请求-响应模型中,紧密耦合的服务带来复杂性和脆弱性

在图8-1中,当用户调用许可证服务时,许可证服务同样需要查找组织数据。许可证服务首先会检查通过组织ID从Redis集群中检索的所需的组织数据。如果许可证服务找不到组织数据,它将使用基于REST的端点调用组织服务,然后在将组织数据返回给用户之前,将返回的数据存储在Redis中。现在,如果有人使用组织服务的REST端点来更新或删除组织记录,组织服务将需要调用在许可证服务上公开的端点,以通知许可证服务使它缓存中的组织数据无效。在图8-1中,如果查看组织服务调用许可证服务以使Redis缓存失效的地方,那么至少可以看到以下3个问题。

(1)组织服务和许可证服务紧密耦合。

(2)耦合带来了服务之间的脆弱性。如果用于使缓存无效的许可证服务端点发生了更改,则组织服务必须要进行更改。

(3)这种方法是不灵活的,因为如果想要为组织服务添加新的消费者,我们必须修改组织服务的代码,才能让它知道需要调用其他的服务以通知数据变更。

1.服务之间的紧密耦合

在图8-1中,我们可以看到许可证服务和组织服务之间存在紧密耦合。许可证服务始终依赖于组织服务来检索数据。然而,通过让组织服务在组织记录被更新或删除时直接与许可证服务进行通信,就已经将耦合从组织服务引入许可证服务了。为了使Redis缓存中的数据失效,组织服务需要许可证服务公开的端点,该端点可以被调用以使许可证服务的Redis缓存无效,或者组织服务必须直接与许可证服务所拥有的Redis服务器进行通信以清除其中的数据。

让组织服务与Redis进行通信有其自身的问题,因为开发人员正直接与另一个服务拥有的数据存储进行通信。在微服务环境中,这是一个很大的禁忌。虽然可以认为组织数据理所当然地属于组织服务,但是许可证服务在特定的上下文中使用这些数据,并且可能潜在地转换数据,或者围绕这些数据构建业务规则。让组织服务直接与Redis服务进行通信,可能会意外地破坏拥有许可证服务的团队所实现的规则。

2.服务之间的脆弱性

许可证服务与组织服务之间的紧密耦合也带来了这两种服务之间的脆弱性。如果许可证服务关闭或运行缓慢,那么组织服务可能会受到影响,因为组织服务正在与许可证服务进行直接通信。同样,如果组织服务直接与许可证服务的Redis数据存储进行对话,那么就会在组织服务和Redis之间创建一个依赖关系。在这种情况下,共享Redis服务器出现任何问题都有可能拖垮这两个服务。

3.在修改组织服务以增加新的消费者方面是不灵活的

这种架构的最后一个问题是,它是不灵活的。使用图8-1中的模型,如果有其他服务对组织数据发生的变化感兴趣,则需要添加另一个从组织服务到该其他服务的调用。这意味着需要更改代码并重新部署组织服务。如果使用同步的请求-响应模型来通知状态更改,则会在应用程序中的核心服务和其他服务之间出现网状的依赖关系模式。这些网络的中心会成为应用程序中的主要故障点。

另一种耦合

虽然消息传递在服务之间增加了一个间接层,但是使用消息传递仍然会在两个服务之间引入紧密耦合。在本章的后面,读者将在组织服务和许可证服务之间发送消息。这些消息将使用JSON作为消息的传输协议,序列化以及反序列化为Java对象。如果两个服务不能优雅地处理同一消息类型的不同版本,则在转换为Java对象时,对JSON消息的结构的变更会造成问题。JSON本身不支持版本控制,但如果读者需要版本控制,那么可以使用Apache Avro。Avro是一个二进制协议,它内置了版本控制。Spring Cloud Stream支持Apache Avro作为消息传递协议。使用Avro不在本书的讨论范围之内,但是本书确实希望让读者意识到,如果真的担心消息版本控制的话,Avro确实会有帮助。

8.1.2 使用消息传递在服务之间传达状态更改

使用消息传递方式将会在许可证服务和组织服务之间注入队列。该队列不会用于从组织服务中读取数据,而是由组织服务用于在组织服务管理的组织数据内发生状态更改时发布消息。图8-2演示了这种方法。

image-20210910142921796

图8-2 当组织状态更改时,消息将被写入位于两个服务之间的消息队列之中

在图8-2所示的模型中,每次组织数据发生变化,组织服务都发布一条消息到队列中。许可证服务正在监视消息队列,并在消息进入时将相应的组织记录从Redis缓存中清除。当涉及传达状态时,消息队列充当许可证服务和组织服务之间的中介。这种方法提供了以下4个好处:

  • 松耦合;
  • 耐久性;
  • 可伸缩性;
  • 灵活性。
1.松耦合

微服务应用程序可以由数十个小型的分布式服务组成,这些服务彼此交互,并对彼此管理的数据感兴趣。正如在前面提到的同步设计中所看到的,同步HTTP响应在许可证服务和组织服务之间产生一个强依赖关系。尽管我们不能完全消除这些依赖关系,但是通过仅公开直接管理服务所拥有的数据的端点,我们可以尝试最小化依赖关系。消息传递的方法允许开发人员解耦两个服务,因为在涉及传达状态更改时,两个服务都不知道彼此。当组织服务需要发布状态更改时,它会将消息写入队列,而许可证服务只知道它得到一条消息,却不知道谁发布了这条消息。

2.耐久性

队列的存在让开发人员可以保证,即使服务的消费者已经关闭,也可以发送消息。即使许可证服务不可用,组织服务也可以继续发布消息。消息将存储在队列中,并将一直保存到许可证服务可用。另一方面,通过将缓存和队列方法结合在一起,如果组织服务关闭,许可证服务可以优雅地降级,因为至少有部分组织数据将位于其缓存中。有时候,旧数据比没有数据好。

3.可伸缩性

因为消息存储在队列中,所以消息发送者不必等待来自消息消费者的响应,它们可以继续工作。同样地,如果一个消息消费者没有足够的能力处理从消息队列中读取的消息,那么启动更多消息消费者,并让它们处理从队列中读取的消息则是一项非常简单的任务。这种可伸缩性方法适用于微服务模型,因为我通过本书强调的其中一件事情就是,启动微服务的新实例应该是很简单的,让这些追加的微服务处理持有消息的消息队列亦是如此。这就是水平伸缩的一个示例。从队列中读取消息的传统伸缩机制涉及增加消息消费者可以同时处理的线程数。遗憾的是,这种方法最终会受消息消费者可用的CPU数量的限制。微服务模型则没有这样的限制,因为它是通过增加托管消费消息的服务的机器数量来进行扩大的。

4.灵活性

消息的发送者不知道谁将会消费它。这意味着开发人员可以轻松添加新的消息消费者(和新功能),而不影响原始发送服务。这是一个非常强大的概念,因为可以在不必触及现有服务的情况下,将新功能添加到应用程序。新的代码可以监听正在发布的事件,并相应地对它们做出反应。

8.1.3 消息传递架构的缺点

与任何架构模型一样,基于消息传递的架构也有折中。基于消息传递的架构可能是复杂的,需要开发团队密切关注一些关键的事情,包括:

  • 消息处理语义;
  • 消息可见性;
  • 消息编排。
1.消息处理语义

在基于微服务的应用程序中使用消息,需要的不只是了解如何发布和消费消息。它要求开发人员了解应用程序消费有序消息时的行为是什么,以及如果消息没有按顺序处理会发生什么情况。例如,如果严格要求来自单个客户的所有订单都必须按照接收的顺序进行处理,那么开发人员必须有区别地建立和构造消息处理方式,而不是每条消息都可以被独立地使用。

这还意味着,如果开发人员正在使用消息传递来执行数据的严格状态转换,那么就需要在设计应用程序时考虑到消息抛出异常或者错误按无序方式处理的场景。如果消息失败,是重试处理错误,还是就这么让它失败?如果其中一个客户消息失败,那么如何处理与该客户有关的未来消息?这些都是需要考虑的问题。

2.消息可见性

在微服务中使用消息,通常意味着同步服务调用与异步处理服务的混合。消息的异步性意味着消息在发布或消费时,它们可能不会被立刻接收或处理。此外,像关联ID这些在Web服务调用和消息之间用于跟踪用户事务的信息,对于理解和调试应用程序中发生的事情是至关重要的。读者可能还记得在第6章中,关联ID是在用户事务开始时生成的唯一编号,并与每个服务调用一起传递,此外,它还应该在每条消息被发布和消费时被传递。

3.消息编排

正如在消息可见性的那部分中提到的,基于消息传递的应用程序更难按照应用程序的执行顺序进行业务逻辑推理,因为它们的代码不再以简单的块请求-响应模型的线性方式进行处理。相反,调试基于消息的应用程序可能涉及多个不同服务的日志,在这些服务中,用户事务可以在不同的时间不按顺序执行。

消息传递可能很复杂但很强大

前面几小节并不是为了吓跑大家,让大家远离在应用程序中使用消息传递。相反,我的目的是强调在服务中使用消息传递需要深谋远虑。我最近完成了一个主要的项目,需要为每个客户开启和关闭有状态的AWS服务器实例集。我们必须使用AWS简单排队服务(Simple Queuing Service,SQS)和Kafka来集成微服务调用和消息的组合。虽然这个项目很复杂,但是在项目结束时,我亲眼看到了消息传递的强大功能。我们的团队意识到我们需要处理的问题是,在服务器被终止之前,我们必须确保从服务器上提取某些文件。这一步骤占据大约75%的用户工作流程,并且整个流程只有在这一步完成之后才能继续进行。幸运的是,我们有一个微服务(称为文件恢复服务),它会检查正在退出的服务器是否已将文件提取出来。由于服务器通过事件传递了所有的状态变化(包括它们正在退出),所以我们只需要将文件恢复服务器插入来自正在退出的服务器的事件流中,并让它们监听“olecommissioning”事件。

如果整个过程都是同步的,那么增加这个文件排查的步骤将是非常痛苦的。但是在最后,我们只需要一个在生产中已存在的现有服务,来监听来自现有消息队列的事件并作出反应。这项工作是在几天内完成的,我们在项目交付过程中从没出过任何差错。通过消息,开发人员可以将服务挂钩在一起,而不需要将服务在基于代码的工作流中硬编码到一起。

8.2 Spring Cloud Stream简介

Spring Cloud可以轻松地将消息传递集成到基于Spring的微服务中,它是通过Spring Cloud Stream项目来实现这一点的。Spring Cloud Stream是一个由注解驱动的框架,它允许开发人员在Spring应用程序中轻松地构建消息发布者和消费者。

Spring Cloud Stream还允许开发人员抽象出正在使用的消息传递平台的实现细节。Spring Cloud Stream可以使用多个消息平台(包括Apache Kafka项目和RabbitMQ),而平台的具体实现细节则被排除在应用程序代码之外。在应用程序中实现消息发布和消费是通过平台无关的Spring接口实现的。

注意

在本章中,读者将使用名为Kafka的轻量级消息总线。Kafka是一种轻量级、高性能的消息总线,允许开发人员异步地将消息从一个应用程序发送到一个或多个其他应用程序。Kafka是用Java编写的,由于Kafka具有高可靠性和可伸缩性,在许多基于云的应用程序中,它已经成为事实上的标准消息总线。此外,Spring Cloud Stream 还支持使用RabbitMQ作为消息总线。Kafka和RabbitMQ都是强大的消息平台,我在本书中选择了Kafka,因为它是我最熟悉的。

要了解Spring Cloud Stream,让我们从Spring Cloud Stream的架构开始讨论,并熟悉Spring Cloud Stream的术语。如果读者以前从未使用过基于消息传递的平台,那么接下来所涉及的新术语可能会有些令人难以理解。

Spring Cloud Stream架构

让我们以通过消息传递进行通信的两个服务的角度来查看Spring Cloud Stream的架构。在这两个服务中,一个是消息发布者,另一个是消息消费者。图8-3展示了如何使用Spring Cloud Stream来帮助消息传递。

image-20210910145929598

图8-3 随着消息的发布和消费,它将流经一系列的Spring Cloud Stream组件,这些组件抽象出底层消息传递平台

随着Spring Cloud中消息的发布和消费,有4个组件涉及发布消息和消费消息,它们是:

  • 发射器(source);
  • 通道(channel);
  • 绑定器(binder);
  • 接收器(sink)。
1.发射器

当一个服务准备发布消息时,它将使用一个发射器发布消息。发射器是一个Spring注解接口,它接收一个普通Java对象(POJO),该对象代表要发布的消息。发射器接收消息,然后序列化它(默认的序列化是JSON)并将消息发布到通道。

2.通道

通道是对队列的一个抽象,它将在消息生产者发布消息或消息消费者消费消息后保留该消息。通道名称始终与目标队列名称相关联。然而,队列名称永远不会直接公开给代码,相反,通道名称会在代码中使用。这意味着开发人员可以通过更改应用程序的配置而不是应用程序的代码来切换通道读取或写入的队列。

3.绑定器

绑定器是Spring Cloud Stream框架的一部分,它是与特定消息平台对话的Spring代码。Spring Cloud Stream框架的绑定器部分允许开发人员处理消息,而不必依赖于特定于平台的库和API来发布和消费消息。

4.接收器

在Spring Cloud Stream中,服务通过一个接收器从队列中接收消息。接收器监听传入消息的通道,并将消息反序列化为POJO。从这里开始,消息就可以按照Spring服务的业务逻辑来进行处理。

8.3 编写简单的消息生产者和消费者

现在我们已经了解完Spring Cloud Stream中的基本组件,接下来看一个简单的Spring Cloud Stream示例。对于第一个例子,我们将要从组织服务传递一条消息到许可证服务。在许可证服务中,唯一要做的事情就是将日志消息打印到控制台。

另外,在这个例子中,因为只有一个Spring Cloud Stream发射器(消息生成者)和接收器(消息消费者),所以我们将要采用Spring Cloud提供的一些便捷方式,让在组织服务中建立发射器以及在许可证服务中建立接收器变得更简单。

8.3.1 在组织服务中编写消息生产者

我们首先修改组织服务,以便每次添加、更新或删除组织数据时,组织服务将向Kafka主题(topic)发布一条消息,指示组织更改事件已经发生。图8-4突出显示了消息生产者,并构建在图8-3所示的通用Spring Cloud Stream架构之上。

image-20210910155410458

图8-4 当组织服务数据发生变化时,它会向Kafka发布消息

发布的消息将包括与更改事件相关联的组织ID,还将包括发生的操作(添加、更新或删除)。

需要做的第一件事就是在组织服务的Maven pom.xml文件中设置Maven依赖项。pom.xml文件可以在organization-service目录中找到。在pom.xml中,需要添加两个依赖项:一个用于核心Spring Cloud Stream库,另一个用于包含Spring Cloud Stream Kafka库。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>

定义完Maven依赖项,就需要告诉应用程序它将绑定到Spring Cloud Stream消息代理。这可以通过使用@EnableBinding注解来标注组织服务的引导类Application(在organization- service/src/main/java/com/thoughtmechanix/organization/Application.java中)来完成。代码清单8-1展示了组织服务的Application类的源代码。

代码清单8-1 带注解的Application

package com.thoughtmechanix.organization;

import com.thoughtmechanix.organization.utils.UserContextFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import javax.servlet.Filter;

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
@EnableBinding(Source.class)  ⇽--- @EnableBinding注解告诉Spring Cloud Stream将应用程序绑定到消息代理
public class Application {
   
    @Bean
    public Filter userContextFilter() {
   
        UserContextFilter userContextFilter = new UserContextFilter();
        return userContextFilter;
    }
    
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值