SpringCloud09—消息总线:Spring Cloud Bus

9.消息总线:Spring Cloud Bus

在微服务架构的系统中,我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以我们称它为消息总线。在总线上的各个实例都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息,例如配置信息的变更或者其他一些管理操作等。

由于消息总线在微服务架构系统中被广泛使用,所以它同配置中心一样,几乎是微服务架构中的必备组件。Spring Cloud作为微服务架构综合性的解决方案,对此自然也有自己的实现,这就是本章我们将要具体介绍的Spring Cloud Bus。通过使用Spring Cloud Bus,可以非常容易地搭建起消息总线,同时实现了一些消息总线中的常用功能,比如,配合Spring Cloud Config 实现微服务应用配置信息的动态更新等。

9.1 消息代理

消息代理(Message Broker)是一种消息验证、传输、路由的架构模式。
它在应用程序之间起到通信调度并最小化应用之间的依赖的作用,使得应用程序可以高效地解耦通信过程。
消息代理是一个中间件产品,它的核心是一个消息的路由程序,用来实现接收和分发消息,并根据设定好的消息处理流来转发给正确的应用。
它包括独立的通信和消息传递协议,能够实现组织内部和组织间的网络通信。
设计代理的目的就是为了能够从应用程序中传入消息,并执行一些特别的操作,下面这些是在企业应用中,我们经常需要使用消息代理的场景:

  • 1.将消息路由到一个或多个目的地
  • 2.消息转化为其他的表现方式
  • 3.执行消息的聚集、消息的分解,并将结果友送到它的目的地,然后重新组合响应返回给消息用户
  • 4.调用Web 服务来检索数据。
  • 5.响应事件或错误。
  • 6.使用发布-订阅模式来提供内容或基于主题的消息路由。

目前Spring cloud bus仅支持两款中间件产品:RabbitMQ和Kafka

9.2 RabbitMQ实现消息总线

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件,也称为面向消息的中间件。RabbitMQ服务器是用高性能、可伸缩而闻名的Erlang语言编写而成的,其集群和故障转移是构建在开放电信平台框架上的。
AMQP是Advanced Message Queuing Protocol的简称,它是一个面向消息中间件的开放式标准应用层协议。它定义了以下这些特性:

  • 消息方向
  • 消息队列
  • 消息路由(包括点到点和发布-订阅模式)可靠性
  • 安全性

AMQP要求消息的提供者和客户端接收者的行为要实现对不同供应商可以用相同的方式(比如SMTP、HTTP、FTP等)进行互相操作。在以往的中间件标准中,主要还是建立在API级别,比如 JMS,集中于通过不同的中间件实现来建立标准化的程序间的互操作性,而不是在多个中间件产品间实现互操作性。

AMQP与JMS不同,JMS定义了一个API和一组消息收发必须实现的行为,而AMQP是一个线路级协议
线路级协议描述的是通过网络发送的数据传输格式。因此,任何符合该数据格式的消息发送和接收工具都能互相兼容和进行操作,这样就能轻易实现跨技术平台的架构方案。
RabbitMQ 以AMQP协议实现,所以它可以支持多种操作系统、多种编程语言,几乎可以覆盖所有主流的企业级技术平台。在微服务架构消息中间件的选型中,它是一个非常适合且优秀的选择。因此,在Spring Cloud Bus中包含了对Rabbit 的自动化默认配置。

基本概念

在具体实现bus 集成RabbitMQ之前我们对其基本的概念先进行简单的了解。

  • Broker:可以理解为消息队列服务器的实体,它是一个中间件应用,负责接收消息生产者的消息,然后将消息发送至消息接收者或者其他的 Broker。
  • Exchange:消息交换机,是消息第一个到达的地方。消息通过它指定的路由规则,分发到不同的消息队列中去。
  • Queue:消息队列,消息通过发送和路由之后最终到达的地方,到达 Queue的消息即进入逻辑上等待消费的状态。每个消息都会被发送到一个或多个队列。
  • Binding:绑定,它的作用就是把Exchange和 Queue按照路由规则绑定起来,也就是Exchange和 Queue之间的虚拟连接。
  • Routing Key:路由关键字,Exchange根据这个关键字进行消息投递。
  • Virtual host:虚拟主机,它是对 Broker的虚拟划分,将消费者,生产者和他们之间依赖的AMQP相关结构进行隔离,一般都是为了安全考虚。比如,我们可以在一个 Broker中设置多个虚拟主机,对不同用户进行权限的分离。
  • Connection:连接,代表生产者、消费者、Broker之间进行通信的物理网络。
  • Channel:消息通道,用于连接生产者和消费者的逻辑结构。在客户端的每个连接里,可建立多个Channel,每个Channel代表一个会话任务,通过Channel可以隔离同一连接中的不同交互内容。
  • Producer:消息生产者,制造消息并发送消息的程序Consumer:消息消费者,接收消息并处理消息的程序。

消息投递到队列的的整个过程大致如下:

  • 1.客户端连接到消息队列服务器,打开一个Channel。
  • 2.客户端声明一个 Exchange,并设置相关属性。
  • 3.客户端声明一个Queue,并设置相关属性。
  • 4.客户端使用Routing Key,在 Exchange和 Queue之间建立好绑定关系。
  • 5.客户端投递消息到Exchange。
  • 6.Exchange接收到消息后,根据消息的Key和已经设置的Binding,进行消息路由,将消息投递到一个或多个Queue 里。

Exchange也有几种类型。

  • 1.Direct交换机:完全根据Key进行投递。比如,绑定时设置了Routing Key为 abc,那么客户端提交的消息,只有设置了Key为abc的才会被投递到队列。
  • 2.Topic交换机:对Key进行模式匹配后进行投递,可以使用符号#匹配一个或多个词,符号*匹配正好一个词。比如,abc.#匹配 abc.def.ghi,abc.*只匹配abc.def。
  • 3.Fanout交换机:不需要任何Key,它采取广播的模式,一个消息进来时,投递到与该交换机绑定的所有队列。

RabbitMQ支持消息的持久化,也就是将数据写在磁盘上。为了数据安全考虑,大多数情况下都会选择持久化。消息队列持久化包括3个部分:

  • 1.Exchange持久化,在声明时指定durable => 1。
  • 2.Queue持久化,在声明时指定durable=>1。
  • 3.消息持久化,在投递时指定delivery_mode=>2(1是非持久化)。

如果 Exchange和 Queue都是持久化的,那么它们之间的 Binding 也是持久化的。如果Exchange和 Queue两者之间有一个是持久化的,一个是非持久化的,就不允许建立绑定。

安装与使用

快速入门

接下来,我们通过在Spring Boot web应用中整合RabbitMQ,实现一个简单的发送、接收消息的例子来对RabbitMQ有一个直观的感受和理解。
在Spring Boot中整合RabbitMQ是一件非常容易的事,因为之前我们已经介绍过StarterPOMs,其中的AMQP模块就可以很好地支持RabbitMQ,下面我们就来详细说说整合过程。

  • 新建一个Spring Boot web工程,命名为rabbitmq-hello。
  • 在pom.xml中引入如下依赖内容,其中 spring-boot-starter-amap用于支持RabbitMQ。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cloud</groupId>
    <artifactId>rabbitmq-hello</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rabbitmq-hello</name>
    <description>rabbitmq-hello</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

    </dependencies>
</project>

  • 在application.properties配置文件中添加以下配置,此出需要自己在rabbitMq的配置中事先添加好用户
spring.application.name=rabbitmq-hello
# 配置rabbbitMq
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=springCloud
spring.rabbitmq.password=123456

创建消息生产者sender。通过注入AmqpTemplate接口的实例来实现消息的发送,AmqpTemplate接口定义了一套针对AMQP协议的基础操作。在 Spring Boot中会根据配置来注入其具体实现。在该生产者中,我们会产生一个字符串,并发送到名为hello 的队列中。

@Component
public class Sender {
    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send() {
        String context = "hello" + new Date();
        System.out.printf("Sender: " + context);
        this.rabbitTemplate.convertAndSend("hello", context);

    }

}

创建消息消费者Receiver。通过@RabbitListener注解定义该类对 hello 队列的监听,并用@RabbitHandler 注解来指定对消息的处理方法。所以,该消费者实现了对hello队列的消费,消费操作为输出消息的字符串内容。

@Component
@RabbitListener(queues = "hello")
public class Receiver {
    @RabbitHandler
    public void process(String hello) {
        System.out.printf("Receiver: " + hello);
    }
}

创建RabbitMQ的配置类RabbitConfig,用来配置队列、交换器、路由等高级信息。这里我们以入门为主,先以最小化的配置来定义,以完成一个基本的生产和消费过程。

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RabbitConfig {
    @Bean
    public Queue helloQueue() {
        return new Queue("hello");
    }
}

接下来我们通过单元测试进行测试

import com.cloud.rabbitmqhello.component.Sender;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class RabbitmqHelloApplicationTests {
    @Autowired
    private Sender sender;

    @Test
    void hello() {
        sender.send();
    }

}

运行结果如下:
在这里插入图片描述

在这里插入图片描述

整合spring Cloud Bus

准备工作:这里我们不创建新的应用,但需要用到Cloud Config 的几个工程,

  • config-repo:定义在Git仓库中的一个目录,其中存储了应用名为didispace的多环境配置文件,配置文件中有一个from参数。
  • config-server:配置了Git仓库,并注册到了Eureka的服务端。
  • config-client:通过Eureka发现为didispace,用来访问配直服务器以获取配置信息。该应用中提供了一个/from接口,它会获取config-repo/didispace-dev.properties中的from属性并返回。

注意以上内容在springCloud08—分布式配置中心:Spring Cloud Config中已经配置好

扩展config-client应用
  • 1.修改pom.xml,增加spring-cloud-starter-bus-amqp模块(注意spring-boot-starter-actuator模块也是必需的,用来提供刷新端点)。
    新增依赖

    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bus-amqp</artifactId>
            </dependency>
    
  • 2.在application.properties文件中增加关于RabbitMQ的连接信息

    # 添加rabbitMq配置
    spring.rabbitmq.host=localhost
    spring.rabbitmq.port=5672
    spring.rabbitmq.username=springCloud
    spring.rabbitmq.password=123456
    
  • 3.启动config-server,再启动两个coonfig-client(分别在不同的端口上,比如7002、7003)。

  • 4.先访问两个config-client 的/from请求,会返回当前config-repo/ didispace-dev.properties中的from属性。
    在这里插入图片描述

  • 5.接着,修改config-repo/didispace-dev.properties中的from属性值,并发送POST请求到其中的一个/bus/refresh。

  • 6.最后,再分别访问启动的两个config-client的/from 请求,此时这两个请求都会返回最新的config-repo/ didi
    space-dev.properties中的from属性。

原理分析

整个架构如下所示:
在这里插入图片描述

此时,若我们需要修改“Service A”的属性。
首先,通过Git 管理工具去仓库中修改对应的属性值,但是这个修改并不会触发“Service A”实例的属性更新。我们向“Service A"的实例3发送POST请求,访问/bus/ refresh接口。此时,“Service A”的实例3就会将刷新请求发送到消息总线中,该消息事件会被“Service A”的实例1和实例2从总线中获取到,并重新从Config Server中获取它们的配置信息,从而实现配置信息的动态更新。
而从Git仓库中配置的修改到发起/bus/refresh的 POST请求这一步可以通过Git仓库的 Web Hook来自动触发。由于所有连接到消息总线上的应用都会接收到更新请求,所以在 Web Hook 中就不需要维护所有节点内容来进行更新,从而解决了上一章中仅通过Web Hook来逐个进行刷新的问题。

指定刷新范围

在上面的例子中,我们通过向服务实例请求Spring Cloud Bus的/bus/refresh接口,从而触发总线上其他服务实例的/refresh。但是在一些特殊场景下,我们希望可以刷新微服务中某个具体实例的配置。

Spring Cloud Bus对这种场景也有很好的支持,/bus/refresh接口提供了一个的应用程序。比如,我们可以请求destination参数,用来定位具体要刷新/bus/refresh?destination=customers:9000,此时总线上的各应用实例会根据destination 属性的值来判断是否为自己的实例名,若符合才进行配置刷新,若不符合就忽略该消息。

destination参数除了可以定位具体的实例之外,还可以用来定位具体的服务。定位服务的原理是通过使用Spring 的 PathMatecher(路径匹配)来实现的,比如/bus/refresh?destination=customers:**,该请求会触发customers服务的所有实例进行刷新。

架构优化

既然Spring Cloud Bus 的/bus/refresh 接口提供了针对服务和实例进行配置更新的参数,那么我们的架构也可以相应做出一些调整。在之前的架构中,服务的配置更新需要通过向具体服务中的某个实例发送请求,再触发对整个服务集群的配置更新。虽然能实现功能,但是这样的结果是,我们指定的应用实例会不同于集群中的其他应用实例,这样会增加集群内部的复杂度,不利于将来的运维工作。比如,需要对服务实例进行迁移,那么我们不得不修改 Web Hook 中的配置等。所以要尽可能地让服务集群中的各个节点是对等的。

因此,我们将之前的架构做了一些调整,如下图所示。
在这里插入图片描述

和之前相比,做出的改动如下:

  • 1.我们在configServer中也引入Spring Cloud Bus,将配置服务端也加入到消息总线中来
  • 2./bus/refresh 请求不在发送到具体的服务实例上,而是发送给Config server,并通过destination参数来指定需要更新配置的服务或实例。

通过上面的改动,我们的服务实例不需要再承担触发配置更新的职责。同时,对于Git的触发等配置都只需要针对Config Server即可,从而简化了集群上的一些维护工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值