⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Spring-Cloud/ZooKeeper-Discovery/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labslabx-25 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

《芋道 ZooKeeper 极简入门》文章中,我们一起完成了 ZooKeeper 的学习,并完成了 ZooKeeper 服务器的搭建。

本文我们来学习 Spring Cloud ZooKeeper 提供的 spring-cloud-zookeeper-discovery 组件,基于 Spring Cloud 的编程模型,接入 ZooKeeper 作为注册中心,实现服务的注册与发现。

2. 注册中心原理

在开始搭建 ZooKeeper Discovery 的示例之前,我们先来简单了解下注册中心的原理。

在使用注册中心时,一共有三种角色:服务提供者(Service Provider)、服务消费者(Service Consumer)、注册中心(Registry)。

在一些文章中,服务提供者被称为 Server,服务消费者被称为 Client。胖友们知道即可。

三个角色交互如下图所示:注册中心原理

① Provider:

  • 启动时,向 Registry 注册自己为一个服务(Service)的实例(Instance)。
  • 同时,定期向 Registry 发送心跳,告诉自己还存活。
  • 关闭时,向 Registry 取消注册

② Consumer:

  • 启动时,向 Registry 订阅使用到的服务,并缓存服务的实例列表在内存中。
  • 后续,Consumer 向对应服务的 Provider 发起调用时,从内存中的该服务的实例列表选择一个,进行远程调用。
  • 关闭时,向 Registry 取消订阅

③ Registry:

  • Provider 超过一定时间未心跳时,从服务的实例列表移除。
  • 服务的实例列表发生变化(新增或者移除)时,通知订阅该服务的 Consumer,从而让 Consumer 能够刷新本地缓存。

当然,不同的注册中心可能在实现原理上会略有差异。例如说,Eureka 注册中心,并不提供通知功能,而是 Eureka Client 自己定期轮询,实现本地缓存的更新。

另外,Provider 和 Consumer 是角色上的定义,一个服务同时即可以是 Provider 也可以作为 Consumer。例如说,优惠劵服务可以给订单服务提供接口,同时又调用用户服务提供的接口。

3. 快速入门

示例代码对应仓库:

本小节,我们来搭建一个 ZooKeeper Discovery 组件的快速入门示例。步骤如下:

  • 首先,搭建一个服务提供者 demo-provider ,注册服务到 ZooKeeper 中。
  • 然后,搭建一个服务消费者 demo-consumer,从 ZooKeeper 获取到 demo-provider 服务的实例列表,选择其中一个示例,进行 HTTP 远程调用。

3.1 搭建服务提供者

创建 labx-25-sc-zookeeper-discovery-demo01-provider 项目,作为服务提供者 demo-provider。最终项目代码如下图所示:

 项目

3.1.1 引入依赖

pom.xml 文件中,主要引入 Spring Cloud ZooKeeper Discovery 相关依赖。代码如下:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>labx-25</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>labx-25-sc-zookeeper-discovery-demo01-provider</artifactId>

<properties>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
</properties>

<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- 引入 SpringMVC 相关依赖,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 引入 Spring Cloud ZooKeeper Discovery 相关依赖,将 ZooKeeper 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
</dependencies>

</project>

引入 spring-cloud-starter-zookeeper-discovery 依赖,将 ZooKeeper 作为注册中心,并实现对它的自动配置。

3.1.2 配置文件

创建 application.yaml 配置文件,添加 ZooKeeper Discovery 配置项。配置如下:

spring:
application:
name: demo-provider # Spring 应用名
cloud:
zookeeper:
connect-string: 127.0.0.1:2181
# ZooKeeper 作为注册中心的配置项,对应 ZooKeeperDiscoveryProperties 配置类
discovery:
root: /services # ZooKeeper 数据存储的根节点,默认为 /services

server:
port: 18080 # 服务器端口。默认为 8080

spring.cloud.zookeeper 配置项,设置 ZooKeeper 客户端的配置,对应 ZooKeeperProperties 配置类。

  • connect-string 配置项,设置 ZooKeeper 服务器的地址。

spring.cloud.zookeeper.discovery 配置项,设置 ZooKeeper Discovery 配置项,对应 ZooKeeperDiscoveryProperties 配置类。

  • root 配置项,设置 ZooKeeper 数据存储的根节点,默认为 /services。稍后,我们来具体演示下哈~

3.1.3 DemoProviderApplication

创建 DemoProviderApplication 类,创建应用启动类,并提供 HTTP 接口。代码如下:

@SpringBootApplication
@EnableDiscoveryClient
public class DemoProviderApplication {

public static void main(String[] args) {
SpringApplication.run(DemoProviderApplication.class, args);
}

@RestController
static class TestController {

@GetMapping("/echo")
public String echo(String name) {
return "provider:" + name;
}

}

}

@SpringBootApplication 注解,被添加在类上,声明这是一个 Spring Boot 应用。Spring Cloud 是构建在 Spring Boot 之上的,所以需要添加。

@EnableDiscoveryClient 注解,开启 Spring Cloud 的注册发现功能。不过从 Spring Cloud Edgware 版本开始,实际上已经不需要添加 @EnableDiscoveryClient 注解,只需要引入 Spring Cloud 注册发现组件,就会自动开启注册发现的功能。例如说,我们这里已经引入了 spring-cloud-starter-zookeeper-discovery 依赖,就不用再添加 @EnableDiscoveryClient 注解了。

拓展小知识:在 Spring Cloud Common 项目中,定义了 DiscoveryClient 接口,作为通用的发现客户端,提供读取服务和读取服务列表的 API 方法。而想要集成到 Spring Cloud 体系的注册中心的组件,需要提供对应的 DiscoveryClient 实现类。

例如说,Spring Cloud Alibaba Nacos Discovery 提供了 NacosDiscoveryClient 实现,Spring Cloud ZooKeeper Discovery 提供了 ZooKeeperDiscoveryClient 实现。

如此,所有需要使用到的地方,只需要获取到 DiscoveryClient 客户端,而无需关注具体实现,保证其通用性。

③ TestController 类,提供了 /echo 接口,返回 provider:${name} 结果。

3.1.4 简单测试

通过 DemoProviderApplication 启动服务提供者。启动完成后,我们使用 ZooKeeper 自带的客户端,查看 ZooKeeper /services 目录下的数据。

# 进入 ZooKeeper 控制台
$ bin/zkCli.sh

# 查看 /services 目录
$ ls /services
[demo-provider]

# 查看 /services/demo-provider 目录
$ ls /services/demo-provider
[80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3]

# 查看服务实例 80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3 的数据
$ get /services/demo-provider/80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3
{"name":"demo-provider","id":"80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3","address":"10.101.16.12","port":18080,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"demo-provider","metadata":{}},"registrationTimeUTC":1591619511852,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}

通过 addressport 属性,可以看到该服务实例的 IP 地址和 PORT 端口。

3.2 搭建服务消费者

创建 labx-25-sc-zookeeper-discovery-demo01-consumer 项目,作为服务提供者 demo-consumer。最终项目代码如下图所示:

 项目

整个项目的代码,和服务提供者是基本一致的,毕竟是示例代码 😜

3.2.1 引入依赖

「3.1.1 引入依赖」一样,只是修改 Maven <artifactId />labx-25-sc-zookeeper-discovery-demo01-consumer,见 pom.xml 文件。

3.2.2 配置文件

创建 application.yaml 配置文件,添加相应配置项。配置如下:

spring:
application:
name: demo-consumer # Spring 应用名
cloud:
zookeeper:
connect-string: 127.0.0.1:2181
# Zookeeper 作为注册中心的配置项,对应 ZookeeperDiscoveryProperties 配置类
discovery:
root: /services # Zookeeper 数据存储的根节点,默认为 /services

server:
port: 28080 # 服务器端口。默认为 8080

「3.1.2 配置文件」基本一致,主要是将配置项目 spring.application.name 修改为 demo-consumer

3.2.3 DemoConsumerApplication

创建 DemoConsumerApplication 类,创建应用启动类,并提供一个调用服务提供者的 HTTP 接口。代码如下:

@SpringBootApplication
// @EnableDiscoveryClient
public class DemoConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(DemoConsumerApplication.class, args);
}

@Configuration
public class RestTemplateConfiguration {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

}

@RestController
static class TestController {

@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;

@GetMapping("/hello")
public String hello(String name) {
// <1> 获得服务 `demo-provider` 的一个实例
ServiceInstance instance;
if (true) {
// 获取服务 `demo-provider` 对应的实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("demo-provider");
// 选择第一个
instance = instances.size() > 0 ? instances.get(0) : null;
} else {
instance = loadBalancerClient.choose("demo-provider");
}
// <2> 发起调用
if (instance == null) {
throw new IllegalStateException("获取不到实例");
}
String targetUrl = instance.getUri() + "/echo?name=" + name;
String response = restTemplate.getForObject(targetUrl, String.class);
// 返回结果
return "consumer:" + response;
}

}

}

@EnableDiscoveryClient 注解,因为已经无需添加,所以我们进行了注释,原因在上面已经解释过。

② RestTemplateConfiguration 配置类,创建 RestTemplate Bean。RestTemplate 是 Spring 提供的 HTTP 调用模板工具类,可以方便我们稍后调用服务提供者的 HTTP API。

③ TestController 提供了 /hello 接口,用于调用服务提供者的 /demo 接口。代码略微有几行,我们来稍微解释下哈。

discoveryClient 属性,DiscoveryClient 对象,服务发现客户端,上文我们已经介绍过。这里我们注入的不是 Nacos Discovery 提供的 NacosDiscoveryClient,保证通用性。未来如果我们不使用 Nacos 作为注册中心,而是使用 Eureka 或则 ZooKeeper 时,则无需改动这里的代码。

loadBalancerClient 属性,LoadBalancerClient 对象,负载均衡客户端。稍后我们会使用它,从 Nacos 获取的服务 demo-provider 的实例列表中,选择一个进行 HTTP 调用。

拓展小知识:在 Spring Cloud Common 项目中,定义了LoadBalancerClient 接口,作为通用的负载均衡客户端,提供从指定服务中选择一个实例、对指定服务发起请求等 API 方法。而想要集成到 Spring Cloud 体系的负载均衡的组件,需要提供对应的 LoadBalancerClient 实现类。

例如说,Spring Cloud Netflix Ribbon 提供了 RibbonLoadBalancerClient 实现。

如此,所有需要使用到的地方,只需要获取到 DiscoveryClient 客户端,而无需关注具体实现,保证其通用性。😈 不过貌似 Spring Cloud 体系中,暂时只有 Ribbon 一个负载均衡组件。

当然,LoadBalancerClient 的服务的实例列表,是来自 DiscoveryClient 提供的。

/hello 接口,示例接口,对服务提供者发起一次 HTTP 调用。

  • <1> 处,获得服务 demo-provider 的一个实例。这里我们提供了两种方式的代码,分别基于 DiscoveryClient 和 LoadBalancerClient。
  • <2> 处,通过获取到的服务实例 ServiceInstance 对象,拼接请求的目标 URL,之后使用 RestTemplate 发起 HTTP 调用。

3.2.4 简单测试

① 通过 DemoConsumerApplication 启动服务消费者。启动完成后,我们使用 ZooKeeper 自带的客户端,查看 ZooKeeper /services 目录下的数据。

# 进入 ZooKeeper 控制台
$ bin/zkCli.sh

# 查看 /services 目录
$ ls /services
[demo-consumer, demo-provider]

注意,服务消费者和服务提供是一种角色的概念,本质都是一种服务,都是可以注册自己到注册中心的。

② 访问服务消费者http://127.0.0.1:28080/hello?name=yudaoyuanma 接口,返回结果为 "consumer:provider:yudaoyuanma"。说明,调用远程的服务提供者成功。

③ 关闭服务提供者后,再次访问 http://127.0.0.1:28080/hello?name=yudaoyuanma 接口,返回结果为报错提示 "获取不到实例",说明我们本地缓存的服务 demo-provider 的实例列表已刷新,没有任何实例。

😈 这里我们并没有演示启动多个服务提供者的测试,胖友可以自己尝试下哟。

4. 多环境配置

同一个服务,我们会部署到开发、测试、预发布、生产等环境中,那么我们需要在项目中,添加不同环境的 ZooKeeper 配置。

一般情况下,开发和测试使用同一个 ZooKeeper,预发布和生产使用另一个 ZooKeeper。那么针对相同的 ZooKeeper,我们怎么实现不同环境的隔离呢?

我们可以通过 spring.cloud.zookeeper.discovery.root 配置项,设置不同 Zookeeper 目录进行数据的存储。例如说,生产环境使用 services-prod 目录,预发布使用 services-pre 目录。

之后,在结合《芋道 Spring Boot 配置文件入门》「6. 多环境配置」小节,使用不同的 application-{env}.yaml 配置文件中,设置不同的 spring.cloud.zookeeper.discovery.root 配置项即可。

具体的代码实例,艿艿这里就偷懒不展示,相信胖友机智的小脑瓜~

666. 彩蛋

至此,我们已经完成 Spring Cloud ZooKeeper 作为注册中心的学习。如下是 Zookeeper Discovery 相关的官方文档:

嘿嘿,实际项目中,还是更为推荐使用 Nacos 或者 Eureka 作为注册中心,感兴趣的胖友可以阅读如下文章:

文章目录
  1. 1. 1. 概述
  2. 2. 2. 注册中心原理
  3. 3. 3. 快速入门
    1. 3.1. 3.1 搭建服务提供者
      1. 3.1.1. 3.1.1 引入依赖
      2. 3.1.2. 3.1.2 配置文件
      3. 3.1.3. 3.1.3 DemoProviderApplication
      4. 3.1.4. 3.1.4 简单测试
    2. 3.2. 3.2 搭建服务消费者
      1. 3.2.1. 3.2.1 引入依赖
      2. 3.2.2. 3.2.2 配置文件
      3. 3.2.3. 3.2.3 DemoConsumerApplication
      4. 3.2.4. 3.2.4 简单测试
  4. 4. 4. 多环境配置
  5. 5. 666. 彩蛋