# 17 配置中心
# 17.1 概念
# 为什么需要配置中心
单体应用,配置写在配置文件中,没有什么大问题。如果要切换环境 可以切换不同的profile(2种方式),但在微服务中。
微服务比较多。成百上千,配置很多,需要集中管理。
管理不同环境的配置。
需要动态调整配置参数,更改配置不停服。
# 配置中心介绍
分布式配置中心包括3个部分:
- 存放配置的地方:git ,本地文件 等。
- config server。从 1 读取配置。
- config client。是 config server 的客户端 消费配置。
《配置中心架构图》
阿里中间件的一篇文章:《一篇好TM长的关于配置中心的文章》
http://jm.taobao.org/2016/09/28/an-article-about-config-center/
配置都不会自己更新,都是需要触发client才去git上拉取的。或者触发 在config-server上查看配置时,才去git上拉取。
# 17.2 使用
- 环境部署之前,将所需的配置信息推送到配置仓库
- 启动配置中心服务端,将配置仓库的配置信息拉取到服务端,配置服务端对外提供RESTful接口
- 启动配置客户端,客户端根据 spring.cloud.config 配置的信息去服务器拉取相应的配置
# git
git地址:https://github.com/yueyi2019/online-taxi-config-profile
创建4个配置文件:
config-client-dev.yml
env: dev
# Config Server
pom
<!-- 配置中心服务端:config-server --> <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>
1
2
3
4
5
6
7
8
9yml
spring:
cloud:
config:
server:
git:
#https://github.com/yueyi2019/online-taxi-config-profile.git
uri: https://github.com/yueyi2019/online-taxi-config-profile
username:
password:
#默认是秒,因为git慢
timeout: 15
2
3
4
5
6
7
8
9
10
11
- 启动类
@EnableConfigServer
测试:
启动eureka,config-server。
访问:
http://localhost:6001/config-client-dev.yml
http://localhost:6001/config-client-dev.properties
http://localhost:6001/config-client-dev.json
2
3
4
5
6
小结
获取配置规则:根据前缀匹配
/{name}-{profiles}.properties
/{name}-{profiles}.yml
/{name}-{profiles}.json
/{label}/{name}-{profiles}.yml
name 服务名称
profile 环境名称,开发、测试、生产:dev qa prd
lable 仓库分支、默认master分支
匹配原则:从前缀开始。
2
3
4
5
6
7
8
9
10
11
换分支:
dev分支上:config-client-dev.yml
#服务端口
server:
port: 8001
env: branch-dev-dev
访问:
http://localhost:6001/dev/config-client-dev.yml
http://localhost:6001/dev/config-client-dev.json
2
3
4
5
6
7
8
9
10
11
不写分支,默认是master。
# Config client(只我们所有的微服务)
discovery方式
- pom
<!-- 配置中心客户端:config-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- application.yml
server:
port: 8011
2
- bootstrap.yml
#应用名称,配置文件名,此时:congif-client-dev.yml
spring:
application:
name: config-client
cloud:
config:
discovery:
enabled: true
# config server 的服务id
service-id: config-server
# 环境
profile: dev
# 分支
label: master
2
3
4
5
6
7
8
9
10
11
12
13
14
- 代码
@Value("${env}")
private String env;
2
访问:
http://localhost:8011/config/env0
看到远程 配置,带过来了。
url方式
spring:
cloud:
config:
# 和下面的discovery互斥
# uri:
# - http://localhost:6001
2
3
4
5
6
第9节课。2020年3月12日。
# 刷新
config-server访问:
http://localhost:6001/dev/config-client-dev.yml
# 手动刷新
在config-client端:
- pom
<!-- 服务监控开启refresh 端口 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2
3
4
5
- Java代码
@RefreshScope
ConfigController上添加
2
3
启动:eureka7900,config-client-8011
访问:
http://localhost:8011/config/env0,发现配置没变 修改git上env配置: http://localhost:8011/config/env0,发现配置还没变 但是: config-server:http://localhost:6001/master/config-client-dev.yml 变了
1
2
3
4
5手动更新操作:
执行: yapi上congig-client:手动刷新配置
1
2再访问:
http://localhost:8011/config/env0,发现配置 改变了
1不加注解@RefreshScope
http://localhost:8011/config2/env01
此时配置不变。没有刷新
原理后面讲。
2
3
4
5
有一个问题:
我们再启动 一个端口8012,这样,有两个config client,8011,8012。(eureka7900,config-server,client 8011,client 8012)
是否2个client 可以变呢?
修改git,刷新8011(yapi,config-client,手动刷新配置),发现8011变,而8012没变。
单独刷新8011(yapi config-client-8011),看8011和8012的变化。
http://localhost:6001/master/config-client-dev.yml
http://localhost:8011/config/env0
http://localhost:8012/config/env0
2
3
4
5
6
所以要引入自动刷新。
看源码:调试refresh。
《config-client-刷新-源码图》
# 自动刷新
- 安装rabbit mq
启动:vm虚拟机。
docker run -d --name="MyRabbitMQ" -p 5672:5672 -p 15672:15672 rabbitmq:management
docker rm -f 容器id
访问http://localhost:15672/
guest,guest
直接启动:docker start MyRabbitMQ
2
3
4
5
6
7
8
9
10
- 在 config client 的pom,config-server也要加。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
2
3
4
- bootstrap.yml
spring:
application:
name: config-client
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
2
3
4
5
6
7
8
测试,启动8011,8012,刷新8011,看8012是否改变。
这样违背了,微服务单一职责性原则。不应该在每个微服务中刷新,配置。
应该刷新config-server。
在config-server中添加 bus,actuator。yml中配置rabbitmq。
修改git配置后:配置中心服务端:http://localhost:6001/master/config-client-dev.yml,可以看到变化。
但是8011,8012中并没有变。
刷新配置server 中bus,yapi,config-server,手动刷新配置。
发现8011遍,8012也变了。
《配置中心动态更新原理》
每个client都有一个队里,server也有一个队列。
注意yapi的刷新地址中 refresh,和bus-refresh的区别。
# 钩子
钩子需要重新写:controller:在client中:WebhookController。
自动刷新源码
《config-bus刷新源码》
不要用自动刷新,别万一哪个配置不对,灾难。
# 17.3 原理
config-server职责:(config-server服务器启动时,会去远程git拉取配置文件。此处质疑,),实际:对于git上配置更新,configserver是在restful请求的时候再更新的。然后提供出 API 供客户端来调用。
验证上面的存疑:启动config-server
我只启动config-sever 的时候,仓库目录,只是单纯的一个文件目录,连git仓库都不算。
config-client职责:启动时去config-server 拿配置,缓存后,自己用。
好多书上说的是:config-server启动时,去拉取git,但我实践后,发现不是这样的。3个条件下才会拉取:
- 访问server 配置。
- 启动client,client获取server。
- 刷新client,或刷新server。
# 17.4 源码
# 服务端源码
请求过来->去git拉取 配置->用controller提供出去。
《config-server-启动-请求-源码图》
# 客户端源码
《config-client-启动-源码》
# 刷新
《config-client-刷新-源码图》
第10课完,2020年3月14日。
上节课差《config-client-启动-源码图》,《config-bus 刷新源码》
# 扩展小知识:
给mq发消息,监听mq消息,给自己发消息,监听自己消息。
# 事件监听机制
基于发布-订阅。1对多。
三要素:
事件:ApplicationEvent,继承自JDK的EventObject,所有事件都将继承他,并通过source得到事件源。
事件发布者:ApplicationEventPublisher和ApplicationEventMulticaster,使用它service就有了发布事件的能力。
事件订阅者:ApplicationListener,继承自jdk的EventListener,所有监听器将继承它。
# 事件的定义
事件:都继承自ApplicationEvent,
spring bus中的事件类,都继承自RemoteApplicationEvent。
AckRemoteApplicationEvent:对特定事件确认的事件。确认远端事件。
EnvironmentChangeRemoteApplicationEvent:环境变更事件。
RefreshRemoteApplicationEvent:刷新事件。刷新远端应用配置的事件。
UnknownRemoteApplicationEvent:未知事件。
public abstract class RemoteApplicationEvent extends ApplicationEvent {
private static final Object TRANSIENT_SOURCE = new Object();
事件源
private final String originService;
事件目的服务(serviceId:appContextId)
private final String destinationService;
事件的全局id
private final String id;
2
3
4
5
6
7
8
9
public class EnvironmentChangeRemoteApplicationEvent extends RemoteApplicationEvent {
private final Map<String, String> values;key:环境变量名,value对应后的值。
2
3
# 事件监听器
1、实现:父类ApplicationListener,
刷新监听器:RefreshListener,监听的事件是:RefreshRemoteApplicationEvent。
public class RefreshListener
implements ApplicationListener<RefreshRemoteApplicationEvent> {
private static Log log = LogFactory.getLog(RefreshListener.class);
private ContextRefresher contextRefresher;
public RefreshListener(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}
2
3
4
5
6
7
8
9
10
通过:ContextRefresher的refresh()执行。回想我们的刷新。
环境变更监听器:EnvironmentChangeListener,知道即可。
# 通道定义
SpringCloudBusClient
/**
* Name of the input channel for Spring Cloud Bus.
*/
String INPUT = "springCloudBusInput";
/**
* Name of the output channel for Spring Cloud Bus.
*/
String OUTPUT = "springCloudBusOutput";
发布
@Output(SpringCloudBusClient.OUTPUT)
MessageChannel springCloudBusOutput();
订阅
@Input(SpringCloudBusClient.INPUT)
SubscribableChannel springCloudBusInput();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bus的监听与发送
@Configuration
@ConditionalOnBusEnabled 启用开关
@EnableBinding(SpringCloudBusClient.class) 绑定通道。
@EnableConfigurationProperties(BusProperties.class)
@AutoConfigureBefore(BindingServiceConfiguration.class)
// so stream bindings work properly
@AutoConfigureAfter(LifecycleMvcEndpointAutoConfiguration.class)
// so actuator endpoints have needed dependencies
public class BusAutoConfiguration
@EventListener(classes = RemoteApplicationEvent.class)
public void acceptLocal(RemoteApplicationEvent event) {
if (this.serviceMatcher.isFromSelf(event)
&& !(event instanceof AckRemoteApplicationEvent)) {
当事件是来自自己,并且不是ack事件,则向消息队列发送消息。
this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build());
}
}
@StreamListener(SpringCloudBusClient.INPUT)
public void acceptRemote(RemoteApplicationEvent event) {
if (event instanceof AckRemoteApplicationEvent) {
if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event)
&& this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(event);
}
// If it's an ACK we are finished processing at this point
return;
}
if (this.serviceMatcher.isForSelf(event)
&& this.applicationEventPublisher != null) {
if (!this.serviceMatcher.isFromSelf(event)) {
this.applicationEventPublisher.publishEvent(event);
}
if (this.bus.getAck().isEnabled()) {
AckRemoteApplicationEvent ack = new AckRemoteApplicationEvent(this,
this.serviceMatcher.getServiceId(),
this.bus.getAck().getDestinationService(),
event.getDestinationService(), event.getId(), event.getClass());
this.cloudBusOutboundChannel
.send(MessageBuilder.withPayload(ack).build());
this.applicationEventPublisher.publishEvent(ack);
}
}
if (this.bus.getTrace().isEnabled() && this.applicationEventPublisher != null) {
// We are set to register sent events so publish it for local consumption,
// irrespective of the origin
this.applicationEventPublisher.publishEvent(new SentApplicationEvent(this,
event.getOriginService(), event.getDestinationService(),
event.getId(), event.getClass()));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 监听小例子
本地给本地发事件。
config-client-diy下:
事件订阅者:
@Configuration
@RemoteApplicationEventScan
public class BusConfiguration {
@EventListener
public void onUserRemoteApplicationEvent(CustomRemoteApplicationEvent event) {
System.out.println("原始服务:"+event.getOriginService()+",内容:"+event.getSource());
}
}
2
3
4
5
6
7
8
9
事件:
public class CustomRemoteApplicationEvent extends RemoteApplicationEvent {
public CustomRemoteApplicationEvent(String content , String originService, String destinationService) {
super(content,originService,destinationService);
}
}
2
3
4
5
6
7
8
事件发布者:
@PostMapping("/publish")
public boolean publishEvent(@RequestBody String content) {
String serviceId = applicationContext.getId();
CustomRemoteApplicationEvent event = new CustomRemoteApplicationEvent(content,serviceId,"destination");
eventPublisher.publishEvent(event);
return true;
}
2
3
4
5
6
7
访问yapi:config-client中:监听例子。
# 消息队列事件
项目:config-client-diy
对于发布/订阅模式模式而言,消息的发送者一般只注重将消息推送到相应的Exchange 对应的Channel中,并不在意订阅者是否成功接收并消费掉某条消息。消息发布者只负责把消息送到队列中,订阅者只负责把消息从队列中取出然后消费,两者在业务逻辑上理应是不存在任何耦合或关联的,这也是发布/订阅模式的职责和优点所在。
启动队列
docker start MyRabbitMQ
1yml
spring: application: name: config-client rabbitmq: host: localhost port: 5672 username: guest password: guest cloud: stream: default-binder: rabbit bindings: input: #交换机名称 destination: stream-des-exchange output: #交换机名称 destination: stream-des-exchange
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18pom
<!-- 总线 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
1
2
3
4
5监听
@EnableBinding(Sink.class) public class MyStreamListener { @StreamListener(Sink.INPUT) public void input(String s){ System.out.println("监听 消息队列 手动的内容 : " + s); } }
1
2
3
4
5
6
7
8
9发送
@EnableBinding(Source.class) @RestController @RequestMapping("/rabbitmq") public class MyStreamSend { @Resource private MessageChannel output; @PostMapping("/send") public String sendTestData(@RequestBody String content) { this.output.send(MessageBuilder.withPayload(content).build()); // 发出消息 return "发送成功"; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14访问:yapi,config-client:diy发送队列。启动2个 80和81,看看效果。
只给80发事件,81也会收到。
Stream是构建消息驱动能力的组件。可以进行基于消息队列的消息通信,使用Spring Integration连接消息中间件以实现事件驱动。Bus基于Stream。
消息队列:异步,解耦合,削峰。
rabbitmq:生产者,消费者,交换器,队列。
Spring Cloud Bus基于rabbitmq(amqp,稳定性,安全性好,金融),kafka(吞吐量达,大数据领域)。