【微服务】- SpringCloud中Config、Bus和Stream
SpringCloud中Config
1.Config的简介
官网
分布式系统面临的问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以套集中式的、动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,. 上百个配置文件的管理…
太多了,需要一个东西把所有的都管理起来,这就有了我们的config
config是什么
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
如何使用
SpringCloud Config分为服务端和客户端两部分。
-
服务端也称为分布式配置中心,它是一个独立的微服务应用,睐连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口
-
客户端则是通过指定的配置中心来管理应用资源,以吸与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容
能做什么
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
- 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
- 将配置信息以REST接口的形式暴露
与git的配合使用
由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式
2.Config服务端的配置和测试
准备
先在自己的github或gitee账号建配置仓库。
我的config仓库
同时要保证项目中有注册中心7001可以在我的仓库中复制
我的项目仓库
在idea中新建项目
- 建项目
项目名称cloud-config-center3344 - 写pom
<dependencies>
<!-- 添加消息总线RabbitMQ支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>wf.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
写yml
server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
uri: https://gitee.com/gyhdx/SpringCloud-Config.git
search-paths:
- springcoud-config
label: master
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
主启动
@SpringBootApplication
@EnableConfigServer
public class ConfigMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigMain3344.class,args);
}
}
测试
什么的内容配置完成,启动7001和3344对项目进行测试
访问http://localhost:3344/master/config-dev.yml
成功
配置的读取规则(怎么知道访问上面的链接就能得到数据)
上面是config官网提供的5种访问的方式,这只介绍常用的三种
/{label}/{application}-{profile}.yml
这是我们上面用到的访问方式
- {label}
就是访问的那个分支master还是其他如dev等分支 - {application}-{profile}.yml
这是一种配置文件的命名规则
{application}是前缀config
{profile}是后缀dev等等,两个之间使用-连接
名称config-dev是可以自定义
/{application}-{profile}.yml
和上面的服务方式类似只是省略服务的分支。该一般默认访问master分支
/{application}/{profile}[/{label}]
就是把分支放在后面。不过不同的是使用以上方式访问返回的是一个json串而不是一个字符串。
总结
label:分支(branch)
name :服务名
profiles:环境(dev/test/prod)
3.config的客户端的配置和测试
不改动的直接在仓库中复制,下面不写
- 建项目
- 写pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>wf.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 写yml
此时的yml是bootstrap.yml因为该yml比application.yml先加载
server:
port: 3355
spring:
application:
name: config-client
cloud:
config:
label: master
name: config
profile: dev
uri: http://localhost:3344
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
- 主启动
- controller
@RestController
@Slf4j
public class ConfigController {
@Value("${config.info}")
private String configInfo;
@GetMapping(value = "/configInfo")
public String getInfo(){
return configInfo;
}
}
测试
启动项目
访问http://localhost:3355/configInfo
完成
问题
上面的内容配置完成虽然可以正常访问,但存在一个问题。就是如果在仓库中改变配置的内容
把圈出来的改为4
访问3344服务端时数据被实时更新了
但是客户端的数据没被更新
重启项目才能刷新数据。
如何实现客户端内容的动态刷新?
实现config客户端动态刷新
先改3355的配置
确保pom中有actuator依赖
在bootstrap.yml中添加监控点
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
在controller添加@RefreshScope注解
@RestController
@Slf4j
@RefreshScope
public class ConfigController {
@Value("${config.info}")
private String configInfo;
@GetMapping(value = "/configInfo")
public String getInfo(){
return configInfo;
}
}
启动项目,修改仓库中的配置,此时访问时,客户端依然不能实时更新数据,但是此时通过cmd发送
完成再次访问客户端就能发现数据更新了,虽然上面的发送可以在客户端不重启的模式下更新数据但是还有很大的限制。想要灵活的实现客户端数据的实时更新,需要下面的知识。
SpringCloud的Bus
Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新。
以下是一种拓扑机制,一个客户机接收到消息,就会给整个群中的机器发送。
Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,
它整合了Java的事件处理机制和消息中间件的功能。
Spring Clud Bus目前支持RabbitMQ和Kafka。
作用
Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器, 可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道
这张图和上面的那张图类似,但不同的是,这里只需要对服务机进行通知信息,通过服务机来通知群组中的其他客户机。
为什么被称为信息总线(bus)
- 什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题, 并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。 - 基本原理
ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
Bus的配置
注意:本次bus是通过RabbitMq来实现消息的发送,故如果需要完成配置。需要在机器上安装RabbitMQ和Erlang。自行百度,这里不给出具体安装流程。
新建项目3366
上面只有一个项目不好演示集群效果,故新建一个客户机3366.
直接从我的仓库中复制即可
- 建项目
- 写pom
- 写bootstrap.yml
- 主启动
- controller
bus刷新全局广播设计思想
两种
- 1)利用消息总线触发一个 客户端/bus/refresh,而刷新所有客户端的配置口
- 2)利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置圆
以上两种设计思想中第二种要好,因为第一种 - 打破了微服务的职责单一性, 因为微服务本身是业务模块,它本不应该承担配置刷新的职责。
- 破坏了微服务各节点的对等性。
- 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改
服务的引入消息总线的支持(3344)
- 改pom
添加RabbitMQ的支持
<!--添加消息总线RabbitMQ支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
- 该yml
server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
uri: https://gitee.com/gyhdx/SpringCloud-Config.git
search-paths:
- springcoud-config
label: master
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "bus-refresh"
添加了
rabbitmq:
host: 192.168.113.6
port: 5672
username: guest
password: guest
开启对rabbitMQ的注册
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "bus-refresh"
暴露监控点让后续的命令好执行
客户端添加消息总线的支持
- 改pom
<!-- 添加消息总线RabbitMQ支持 在第一次演示时不要添加该依赖不然不配置相关环境会出问题 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
- 改yml
server:
port: 3366
spring:
application:
name: config-client
cloud:
config:
label: master
name: config
profile: dev
uri: http://localhost:3344
rabbitmq:
host: 192.168.113.6
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
同服务端
测试
完成上述配置启动7001(注册中心)3344、3355、3366
访问http://localhost:3344/master/config-dev.yml、http://localhost:3355/configInfo、http://localhost:3366/configInfo可以看到三个链接访问结果相同都是
此时修改远程仓库中的version
并提交,会发现3344端口同步数据,但是3355、3366的数据没变化,此时(要开启RabbitMQ),通过命令行向3344端口发信息
curl -X POST "http://localhost:3344/actuator/bus-refresh"
再次访问3355、3366会发现数据已经同步。
bus实现定点通知
指定具体某一个实例生效而不是全部
公式: http://localhost:配置中心的端口号/actuator/bus-refresh/ {destination}
/bus/refresh请求不再发送到具体的服务实例上,而是发给config server通过destination参数类指定需要更新配置的服务或实例
测试
修改远程仓库中的version,3344同步3355、3366不同步通过命令行发送curl -X POST “http://localhost:3344/actuator/bus-refresh/config-client:3355”
在实现3355、3366的链接会发现3355同步数据,而3366没有。
SpringCloud中的Stream
官网地址:https://spring.io/projects/spring-cloud-stream#overview
我在这使用的是rabbitmq,stream整合RabbitMQ的官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-stream-binder-rabbit/2.2.1.RELEASE/spring-cloud-stream-binder-rabbit.html
1.为什么会出现stream
上面的bus中为了解决全局动态刷新问题引入了RabbitMQ信息中间件,然而此处有一个问题,我们现在的系统分为了三个部分前端,后端和大数据处理
但是我们可能会在后端和大数据处理部分使用不同的信息中间件。但是各个信息中间件之间是不能通信的,故为了解决不同信息中间件之间的通信问题我们引入了Stream。
stream让我们不再关注具体MQ的细节我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换,
总结
屏蔽底层消息中间件的差异降低切换成本,统一消息的编程模型
2.什么是SpringCloudStream
官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架。
应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象交互。
通过我们配置来binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互。
所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。
Spring Cloud Stream为-些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。
目前仅支持RabbitMQ、Kafka.其他几种MQ有其他的框架支持,我们后续在讲。
3.Stream设计思想
标准MQ的通信方式
- 生产者/消费者之间靠消息媒介传递信息内容:Message(pub)
- 消息必须走特定的通道:消息通道MessageChannel(queue)
- 消息通道里的消息如何被消费呢,谁负责收发处理:消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消 息处理器所订阅(sub)
为什么用Cloud Stream
比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,
像RabbitMQ有exchange, kafka有Topic和Partitions分区,故我们需要一种东西屏蔽不同信息中间件的差异
这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中-种,后面的业务需求,想往另外一种消息队列进行廷移,这时候无疑就是一个灾难性的, 一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候springcloud Stream给我们提供了一种解耦合的方式。
stream凭什么可以统一底层差异?
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,
由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性
通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道, 使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder
INPUT对应于消费者
OUTPUT对应于生产者
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Stream中的消息通信方式遵循了发布-订阅模式
Topic主题进行广播
- 在RabbitMQ就是Exchange
- 在Kakfa中就是Topic
4.stream的标准流程
官方架构图,图1
对信息方式及处理流程,图2
- Binder:很方便的连接中间件,屏蔽差异
- Channel:通道是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
- Source和Sink:简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
编码常用注解
5.案例配置
消息生产者
模块名称:cloud-stream-rabbitmq-provider8801
没给的代码自己到远程仓库中找,我只给出重要的代码
- 建项目
- 写pom
- 写yml
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: #需要绑定的rabbitmq的服务信息
defaultRabbit: #定义的名称,用于binding整合
type: rabbit #消息组件类型
environment: #环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json
binder: defaultRabbit #设置要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #设置跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 #如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示 主机名称
prefer-ip-address: true #访问的路径变为IP地址
- 主启动
- service
public interface IMerssageProvider {
String send();
}
//消息的发送者故绑定Source,图2中有给出
@EnableBinding(Source.class)
public class MessagerProviderImpl implements IMerssageProvider {
//消息是通过通道发送的故引入MessageChannel
@Resource
private MessageChannel output;
@Override
public String send() {
String seral = UUID.randomUUID().toString();
//官网给出的使用
output.send(MessageBuilder.withPayload(seral).build());
System.out.println("-*-*-*-*-serial:" + seral);
return null;
}
}
我没设置访问头
- controller
@RestController
public class ProviderController {
@Resource
private IMerssageProvider merssageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage(){
merssageProvider.send();
return "发送成功!!";
}
}
启动7001、RabbitMQ和8801后会在RabbitMQ中看到
访问http://localhost:8801/sendMessage
在后台显示
表示消息生产者配置成功
消息消费者
模块名称:cloud-stream-rabbitmq-consumer8802
- 建项目
- 写pom
- 写yml
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: #需要绑定的rabbitmq的服务信息
defaultRabbit: #定义的名称,用于binding整合
type: rabbit #消息组件类型
environment: #环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json
binder: defaultRabbit #设置要绑定的消息服务的具体设置
# group: wf #这里和下面的8803项目都把group注释掉,不然无法得到与下面测试相同的结果
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2
lease-expiration-duration-in-seconds: 5
instance-id: receive-8802.com
prefer-ip-address: true
- 主启动
- controller
@Controller
//消费者故绑定Sink
@EnableBinding(Sink.class)
public class StreamConsumerLisnterController {
@Value("${server.port}")
private String serverPort;
//消费者是输入流故在此声明,stream会把与生产者同topic的等待发送到有改注解的方法
@StreamListener(Sink.INPUT)
//要接受消息故在形参表明
public void input(Message<String> message){
//生产者的信息使用了MessageBuilder.withPayload(seral).build()
//故在此get就好
String payload = message.getPayload();
System.out.println("消费者1,------>接收到的信息:" + payload +", port:" + serverPort);
}
}
再新建与8802相似的项目8803与8802组成一个集群。就只需要改变yml中的端口号就好。这里不给出具体流程
测试
启动7001、8801、8802、8803和rabbitMQ
启动完成查看rabbitMQ
表示配置成功。发送消息
然后发送消息http://localhost:8801/sendMessage观察后台
会发现8801发送的消息8802和8803都接收到了,这就引出了我们下一个问题
持续消费问题
目前是8802/8803同时都收到了,存在重复消费问题。因为8802和8803属于同一个集群,故如果有一个消息进来应该是集群中的每一个访问进行处理,而不是集群中所以访问都去处理一个问题,如再存钱的时候应该的负责存钱的集群中一个服务来处理钱的存入,而不是所有服务都去处理,不然就会造成输入一次,存多次钱的问题,这样是不行的,
如果解决上述的问题,给服务分组即我在上面创建8802的yml中的注释,因为在stream中如果属于同一个组那么消息就只能被组中某一个服务得到。
而如果我们不再yml配置中设置组,那么stream就会给该服务设置一个默认的组
解决持续消费问题
给8802和8803添加group属性(把注释掉的group放开)
再启动所有项目访问 localhost:8801/sendMessage
生产者发送多条消息
消费者进行消费
可以看到重复消费问题被解决了。
消息的持久化问题
只要在yml配置中给服务配置了group属性那么stream就会自动进行持久化
我们把8802和8803服务都停掉,把两个服务中8802中的group属性注释掉,8803不注释访问localhost:8801/sendMessage
然后启动8802、8803
会发现
8802没有接受8801发送的消息,而8803接收到了这就是消息的持久化。