1.因为zookeeper的数据结构比较简单,而且与传统的磁盘文件系统不同的是,zk将全量数据存储在内存中,可谓是高性能,并且支持集群,另外还支持事件监听。这些特点决定了zk特别适合作为注册中心(数据发布/订阅)。
2.zookeeper在cap领域中占据了数据一致性和分区容错性,这让zookeeper在拥有高数据一致性的需求情况下,有很大的优势,使我们在这种保证高数据一致性的需求下,能够满足我们的需求
这里我使用的开发软件是idea,,如果有用到eclipse作为开发软件的话,那请创建一个maven项目,将我下面的pom文件复制过去,然后创建响应的目录结构即可
1.使用idea新建项目,选择Spring Initializr,点击next
2.填写maven项目结构信息,点击next
3.这里使用spring boot 2.3.10版本,然后选中以下图中组件,点击next
4.修改项目构建目录等信息,点击finish
5.项目结构如下
注意: 这里我的zookeeper服务通过我之前写的博客搭建的,博客地址如下
: docker篇-(docker-compose安装zookeeper集群,并使用nginx实现负载均衡)
这里现在pom文件里面把spring-cloud-starter-zookeeper-config的依赖注释调,因为spring-cloud-starter-config需要的配置优先级比较高,需要加载bootstrap.yml里面的配置,所以这里先把组件先注释调,如下图
1.创建application.yml文件,创建如下配置信息
spring:
application:
name: product
cloud:
zookeeper:
connect-string: 192.168.101.180:2181
server:
port: 9090
package com.lhstack.cloud.product.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lhstack
*/
@RestController
@RequestMapping
public class ProductController {
@GetMapping("product")
public String product(){
return "this is product";
}
}
这里新增ribbon的依赖
同理,在开始之前,我们也先把config相关的依赖给注释掉
spring:
application:
name: consumer
cloud:
zookeeper:
connect-string: 192.168.101.180:2181
server:
port: 8080
因为这里要获取服务,所以需要使用EnableDiscoveryClient注释来开启服务发现
package com.lhstack.cloud.consumer.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author lhstack
*/
@RestController
@RequestMapping
public class ConsumerController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("consumer")
public String consumer(){
ServiceInstance serviceInstance = loadBalancerClient.choose("product");
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(serviceInstance.getUri().toString() + "/product",String.class);
}
}
spring:
application:
name: product
cloud:
zookeeper:
connect-string: 192.168.101.180:2181 #这里多个地址,用,隔开即可
discovery:
metadata:
version: v1 #这个配置,就是在注册服务之后,可以添加一些业务数据,如版本,如region等等,可以在后续开发gateway的过程中,或者使用loadbalancer实现灰度发布,蓝绿发布等等的功能
root: /discovery #这里,设置服务注册的跟节点,查看zkui,默认的根节点是/services,可以设置成多级目录,如第一级目录为项目名,第二级目录为环境名等等,如/cms/dev,则就是cms项目dev环境,千万不要使用spring.cloud.zookeeper.prefix,我也不知道为什么,加了这个参数之后,就无法发现服务了
server:
port: 9090
...
public class ZookeeperAutoServiceRegistrationAutoConfiguration {
...
//我们在这里可以扩展实现自己的ServiceInstanceRegistration
//在这里我透露一下,spring-cloud-starter-zookeeper-discovery默认创建的zookeeper节点是临时节点,如果不能满足需求,可以自己通过这种方式扩展
//这里我说明一下,zookeeper的零时节点是根据session失效来实现的,除了服务宕机,基本就不会出现服务下线的情况,这也是zookeeper的特点所在,通过session来实现心跳的功能,性能会比http协议高很多,因为zookeeper是基于tcp协议的,同时本身就附带心跳检测功能
@Bean
@ConditionalOnMissingBean({ZookeeperRegistration.class})
public ServiceInstanceRegistration serviceInstanceRegistration(ApplicationContext context, ZookeeperDiscoveryProperties properties) {
String appName = context.getEnvironment().getProperty("spring.application.name", "application");
String host = properties.getInstanceHost();
if (!StringUtils.hasText(host)) {
throw new IllegalStateException("instanceHost must not be empty");
} else {
properties.getMetadata().put("instance_status", properties.getInitialStatus());
ZookeeperInstance zookeeperInstance = new ZookeeperInstance(context.getId(), appName, properties.getMetadata());
//这里,就是构建我们注册的服务实例信息
RegistrationBuilder builder = ServiceInstanceRegistration.builder().address(host).name(appName).payload(zookeeperInstance).uriSpec(properties.getUriSpec());
if (properties.getInstanceSslPort() != null) {
builder.sslPort(properties.getInstanceSslPort());
}
if (properties.getInstanceId() != null) {
builder.id(properties.getInstanceId());
}
return builder.build();
}
}
}
...
public class ServiceDiscoveryImpl<T> implements ServiceDiscovery<T> {
...
//这个方法,就是向zookeeper里面写入服务注册的信息
@VisibleForTesting
protected void internalRegisterService(ServiceInstance<T> service) throws Exception {
byte[] bytes = this.serializer.serialize(service);
String path = this.pathForInstance(service.getName(), service.getId());
int MAX_TRIES = true;
boolean isDone = false;
//这里,我们之前看到的服务builder的那个类,默认CreateMode就是EPHEMERAL节点
for(int i = 0; !isDone && i < 2; ++i) {
try {
CreateMode mode;
switch(service.getServiceType()) {
case DYNAMIC:
mode = CreateMode.EPHEMERAL;
break;
case DYNAMIC_SEQUENTIAL:
mode = CreateMode.EPHEMERAL_SEQUENTIAL;
break;
default:
mode = CreateMode.PERSISTENT;
}
((ACLBackgroundPathAndBytesable)this.client.create().creatingParentContainersIfNeeded().withMode(mode)).forPath(path, bytes);
isDone = true;
} catch (NodeExistsException var8) {
this.client.delete().forPath(path);
}
}
}
public void unregisterService(ServiceInstance<T> service) throws Exception {
ServiceDiscoveryImpl.Entry<T> entry = (ServiceDiscoveryImpl.Entry)this.services.remove(service.getId());
this.internalUnregisterService(entry);
}
public ServiceProviderBuilder<T> serviceProviderBuilder() {
return (new ServiceProviderBuilderImpl(this)).providerStrategy(new RoundRobinStrategy()).threadFactory(ThreadUtils.newThreadFactory("ServiceProvider"));
}
...
}
...
//这里,实现了spring提供的DiscoveryClient,然后内部调用ServiceDiscovery,就是上面那个服务注册那个类
public class ZookeeperDiscoveryClient implements DiscoveryClient {
...
private final ServiceDiscovery<ZookeeperInstance> serviceDiscovery;
}
因为application.yml是spring cloud应用初始化之后加载的配置文件,而bootstrap.yml是spring cloud应用初始化之前加载的配置,由于应用需要加载配置,所以spring-cloud-strater-zookeeper-config在加载的时候,是加载的bootstrap.yml的文件内容,保证spring cloud应用在初始化之前就已经拿到配置
spring:
profiles:
active: dev #指定dev环境
application:
name: product
cloud:
zookeeper:
config:
root: /discovery/config
profileSeparator: "," #这是与环境连接的分隔符,如现在配置的是dev环境,那么加载的配置文件名称就是/discovery/config/product,dev /discovery/config/product /discovery/application,dev /discovery/application 优先级是环境,其次是spring.application.name然后再是 defaultContext,defaultContext是用来做默认配置的,用于比如全局的通用信息等等
defaultContext: application #这是默认加载的命名空间
connect-string: 192.168.101.180:2181 #这里多个地址,用,隔开即可
1.将注册信息放在application,dev环境下面,这种信息应该由当前项目当前环境的所有服务共享
2.将特定信息放在当前服务的特定环境下面
package com.lhstack.cloud.product.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lhstack
*/
@RestController
@RequestMapping
@RefreshScope //开启属性更新功能,让这个bean里面的属性会根据配置中心的修改而同步
public class ProductController {
@Value("${msg:hello}")
private String msg;
@GetMapping("product")
public String product(){
return msg;
}
}
修改zookeeper里面product,dev环境下面的msg的值,然后在访问接口
这里修改成2
查看控制台的日志,修改的key也打印出来了
对应的pom文件也要取消注释
application.yml也要重命名为bootstrap.yml
spring:
profiles:
active: dev
application:
name: consumer
cloud:
zookeeper:
config:
root: /discovery/config
profileSeparator: "," #这是与环境连接的分隔符,如现在配置的是dev环境,那么加载的配置文件名称就是/discovery/config/consumer,dev /discovery/config/consumer /discovery/application,dev /discovery/application
defaultContext: application #这是默认加载的命名空间
connect-string: 192.168.101.180:2181 #这里多个地址,用,隔开即可
server:
port: 8080
zookeeper实现配置中心的原理,大家有兴趣可以去查看ZookeeperConfigAutoConfiguration和ZookeeperConfigBootstrapConfiguration两个类,ZookeeperConfigBootstrapConfiguration的作用是用于第一次启动的时候,去加载zookeeper里面的配置,然后记录所有加载的keys,交给ZookeeperConfigAutoConfiguration去创建ConfigWatcher监听对应的keys,通过监听zk里面的更新事件,去更新对应的keys,当然,要更新属性的对象,必须加上RefreshScope注解