一、背景
在微服务架构的系统中,我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以我们称它为消息总线。
Spring Cloud 作为微服务架构综合性的解决方案,对此自然也有自己的实现,这就是我们本章要具体介绍的Spring Cloud Bus.
二、消息代理.
消息代理是一种消息验证、传输、路由的架构模式。它在应用程序之间起到通信调度并最小化应用之间的依赖的作用,使得应用程序可以高效地解耦通信过程。消息代理是一个中间件产品,它的核心是一个消息的路由程序,用来实现接收和分发消息,并根据设定好的消息处理流来转发给正确的应用。我们经常需要使用消息代理的场景:
- 将消息路由到一个或多个目的地。
- 消息转化为其他的表现方式。
- 执行消息的聚集、消息的分解,并将结果发送到它们的目的地,然后重新组合响应返回给消息用户。
- 调用 Web 服务来检索数据。
- 响应事件或错误。
- 使用发布-订阅模式来提供内容或基于主题的消息路由。
目前已经有非常多的开源产品可以供大家使用,比如:
- ActiveMQ
- Kafka
- RabbitMQ
- RocketMQ
- …
当前版本的 Spring Cloud Bus 仅支持两款中间件产品:RabbitMQ 和 Kafaka。在下面的章节中,我们将分别介绍如何使用这两款消息中间件与 Spring Cloud Bus 配合实现消息总线。
三、RabbitMQ 实现消息总线
RibbitMQ 是实现了高级消息队列协议(AMQP)的开源消息代理软件,也称为面向消息的中间件。
1.基本概念
- Broker
- Exchange
- Queue
- Binding
- Routing Key
- Virtual host
- Connection
- 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进行投递。
2、Topic交换机:对Key进行模式匹配后进行投递,可以使用符号#匹配一个或者多个词,符号*匹配正好一个词。
3、Fanout 交换机:不需要任何Key,它采取广播的模式,一个消息进来时,投递到与该交换机绑定的所有队列。
RabbitMQ 支持消息的持久化,也就是将数据写在磁盘上。
2、安装与使用
省略.
3、快速入门
接下来,我们通过在Spring Boot 应用中整合RabbitMQ,实现一个简单的发送、接收消息的例子来对RabbitMQ有一个直观的感受和理解.
- 新建一个Spring Boot 工程,命名为 rabbitmq-hello
- 在pom中引入依赖,其中 spring-boot-starter-amqp用于支持RabbitMQ
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gildata</groupId>
<artifactId>rabbit-hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rabbit-hello</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 在 application。properties 中配置关于RabbitMQ的连接和用户信息。
spring.application.name=rabbitmq-hello
spring.rabbitmq.host=localhost
spring.rabbitmq.prot=5672
spring.rabbitmq.username=springcloud
spring.rabbitmq.password=gildata@123
- 创建消息生产者 Sender .通过注入 AmqpTemplate 接口的实例来实现消息的发送
@Component
public class Sender {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send(){
String context = "hello " + new Date();
System.out.println("Sender : " + context );
this.rabbitTemplate.convertAndSend("hello",context);
}
}
- 创建消息消费者 Receiver 。 通过@RabbitListener 注解定义该类型对 hello 队列的监听,并用@RabbitHandler 注解来指定对消息的处理方法。
/**
* Created by liaock on 2019/1/30
**/
@Component
@RabbitListener(queues = "hello")
public class Receiver {
@RabbitHandler
public void process(String hello){
System.out.println("Receiver : " + hello);
}
}
- 创建RabbitMQ 的配置类 RabbitConfig. 用来配置队列、交换器、路由等高级信息。
/**
* Created by liaock on 2019/1/30
**/
public class RabbitConfig {
@Bean
public Queue helloQueue(){
return new Queue("hello");
}
}
- 创建应用主类
@SpringBootApplication
public class RabbitHelloApplication {
public static void main(String[] args) {
SpringApplication.run(RabbitHelloApplication.class, args);
}
}
- 创建单元测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitHelloApplicationTests {
@Autowired
private Sender sender;
@Test
public void contextLoads() {
sender.send();
}
}
运行单元测试类, 测试成功 !
4、整合Spring Cloud Bus
- 准备工作:用到上一章中已经实现的关于Spring Cloud Config的几个工程。
- config-repo:定义在Git仓库中的一个目录,其中存储了多环境配置文件,配置文件中有一个from参数。
- config-server-eureka:配置了Git仓库,并注册到了Eureka的服务端。
- config-client-eureka:通过Eureka发现Config Server的客户端,用来访问配置服务器以获取配置信息。该应用中提供了一个/from接口,它会获取properties中的from属性返回。
- 扩展config-client-eureka应用
- 修改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>
- 在配置文件中增加关于RabbitMQ的连接和用户信息
spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=springcloud spring.rabbitmq.password=gildata@123
- 启动config-server-eureka,再启动两个config-client-eureka(分别在不同的端口上,比如7002、7003),我们可以在config-client-eureka中的控制台中看到如下内容,在启动时候,客户端程序多了一个/bus/refresh请求。
- 先访问两个config-client-eureka的/from请求,会返回当前properties中的from属性。
- 接着,我们修改properties中的from属性值,并发送POST请求到其中的一个/bus/refresh。
- 最后,再分别访问启动的两个config-client-eureka的/from请求,此时这两个请求都会返回最新的properties中的from属性。
到这里已经能够通过Spring Cloud Bus来实时更新总线上的属性配置了。
5、原理分析
如图所示,我们向 service A 的3实例 发送POST请求,访问 /acuator/bus/refresh 接口, 此时 Service A 3实例 就会将刷新请求发送到消息总线中,该消息事件会被 Service A 的实例1和实例2从总线中获取到,并重新从 Config Server中获取到它们的配置信息,从而实现配置信息的动态刷新。
6、架构优化
- 在Config Server中也引入Spring Cloud Bus,将配置服务端也加入到消息总线中来。
- acuator/bus-refresh请求不再发送到具体服务实例上,而是发送给Config Server,并通过destination参数来指定需要更新配置的服务或实例。
通过上面的改动,服务实例就不需要再承担触发配置更新的职责。同时,对于Git的触发等配置都只需要针对Config Server即可,从而简化了集群上的一些维护工作。
四、Kafka 实现消息总线
1.简介
Kafka是基于消息发布/订阅模式实现的消息系统,其主要设计目标如下:
- 消息持久化:以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。
- 高吞吐:在廉价的商用机器上也能支持单机每秒100K条以上的吞吐量
- 分布式:支持消息分区以及分布式消费,并保证分区内的消息顺序
- 跨平台:支持不同技术平台的客户端(如:Java、PHP、Python等)
- 实时性:支持实时数据处理和离线数据处理
伸缩性:支持水平扩展
Kafka中涉及的一些基本概念:
- Broker:Kafka集群包含一个或多个服务器,这些服务器被称为Broker。
- Topic:逻辑上同Rabbit的Queue队列相似,每条发布到Kafka集群的消息都必须有一个Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个Broker上,但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
- Partition:Partition是物理概念上的分区,为了提供系统吞吐率,在物理上每个Topic会分成一个或多个Partition,每个Partition对应一个文件夹(存储对应分区的消息内容和索引文件)。
- Producer:消息生产者,负责生产消息并发送到Kafka Broker。
- Consumer:消息消费者,向Kafka Broker读取消息并处理的客户端。
- Consumer Group:每个Consumer属于一个特定的组(可为每个Consumer指定属于一个组,若不指定则属于默认组),组可以用来实现一条消息被组内多个成员消费等功能。
2、整合Spring Cloud Bus
要使用Kafka来实现消息总线时,只需要把spring-cloud-starter-bus-amqp替换成spring-cloud-starter-bus-kafka模块,在pom.xml的dependenies节点中进行修改
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
如果在启动Kafka时均采用了默认配置,那么不需要再做任何其他配置就能在本地实现从RabbitMQ到Kafka的切换。