基于Zookeeper实现服务注册和服务发现功能

 更新时间:2023年09月07日 10:12:43   作者:叫我二蛋  
无论是采用SOA还是微服务架构,都需要使用服务注册和服务发现组件,本文将基于 Zookeeper 实现服务注册和服务发现功能,如果跟我一样有同样的困惑,希望可以通过本文了解其他组件如何使用 Zookeeper 作为注册中心的工作原理

前言

无论是采用SOA还是微服务架构,都需要使用服务注册和服务发现组件。我刚开始接触 Dubbo 时一直对服务注册/发现以及 Zookeeper 的作用感到困惑,现在看来是因为对分布式系统的理解不够深入,对 Dubbo 和 Zookeeper 的工作原理不够清楚。

本文将基于 Zookeeper 实现服务注册和服务发现功能,如果跟我一样有同样的困惑,希望可以通过本文了解其他组件如何使用 Zookeeper 作为注册中心的工作原理。

声明

文章中所提供的代码仅供参考,旨在帮助缺乏基础知识的开发人员更好地理解服务注册和服务发现的概念。请注意,这些代码并不适用于实际应用中

前置知识

服务注册和发现

在SOA或微服务架构中,由于存在大量的服务以及可能的相互调用,为了更有效地管理这些服务,我们通常需要引入一个统一的地方,即注册中心,来集中管理它们,而注册中心最基本的功能就是服务注册/发现。

  • 服务注册:将该服务实例的元数据(如IP地址、端口号、健康状态等)注册到注册中心,这样其他服务或客户端可以发现和使用该服务。
  • 服务发现:当一个服务需要调用别的服务时,使用静态配置是不可行的,这个时候可以去注册中心获取可用的服务实例并调用。

Zookeeper

Zookeeper 是一个传统的分布式协调服务,它更多的被用来作为一个协调器使用,比如来协调管理 Hadoop 集群、协调 Kafka 的 leader 选举等。

为什么会有组件将其视为一个注册中心使用?我想有几个原因:

  1. Zookeeper 在分布式系统中具有更强的一致性和可靠性,可以确保各个服务的注册信息保持一致。
  2. Zookeeper 使用内存存储数据,具有很高的读写性能。这对于注册中心来说非常关键,因为它需要快速地响应客户端的请求。
  3. Zookeeper 的 Watcher 机制可以让客户端监听指定节点的变化。当某个节点(注册中心)发生变化时,Zookeeper 可以通知其他服务实现实时更新。

工作原理

以下图为例,可以看到 Dubbo 是如何使用 Zookeeper 实现服务注册/发现的。

  • 服务提供者向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址。
  • 服务消费者订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址。

这里的目录就是 Zookeeper 的数据结构,原理非常简单,本质上就是服务提供者和消费者按照约定在 Zookeeper 上读写数据,同时借用其 Watcher 机制、临时节点和可靠性等特性高效的实现以下功能:

  • 当提供者服务出现断电等异常停机时,注册中心能自动删除提供者信息。
  • 当注册中心重启时,能自动恢复注册数据以及订阅请求。

实现过程

注册中心

下面通过 Zookeeper 的 Java API 实现一个只包含服务注册/发现的注册中心,代码如下:

public class RegistrationCenter {
    // 连接信息
    private String connectString = "192.168.10.11:2181,192.168.10.11:2182,192.168.10.11:2183";
    // 超时时间
    private int sessionTimeOut = 30000;
    private final String ROOT_PATH = "/servers";
    private ZooKeeper client;
    public RegistrationCenter() {
        this(null);
    }
    public RegistrationCenter(Consumer<List<String>> consumer) {
        try {
            getConnection(null == consumer ? null : watchedEvent -> {
                //监听服务器地址的上下线
                if (watchedEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged) {
                    try {
                        consumer.accept(subServers());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            Stat stat = client.exists(ROOT_PATH, false);
            if (stat == null) {
                //创建根节点
                client.create(ROOT_PATH, ROOT_PATH.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * @param serverName 将服务器注册到zk集群时,所需的服务名称
     * @param metadata   服务元数据
     * @throws Exception
     */
    public void doRegister(String serverName, Metadata metadata) throws Exception {
        /**
         * ZooDefs.Ids.OPEN_ACL_UNSAFE: 此权限表示允许所有人访问该节点(服务器)
         * CreateMode.EPHEMERAL_SEQUENTIAL: 由于服务器是动态上下线的,上线后存在,下线后不存在,所以是临时节点
         * 而服务器一般都是有序号的,所以是临时、有序的节点.
         */
        String node = client.create(ROOT_PATH + "/" + serverName, metadata.toString().getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(serverName + " 已经上线");
    }
    /**
     * 发现/订阅服务
     */
    public List<String> subServers() throws InterruptedException, KeeperException {
        List<String> zkChildren = client.getChildren(ROOT_PATH, true);
        List<String> servers = new ArrayList<>();
        zkChildren.forEach(node -> {
            //拼接服务完整信息
            try {
                byte[] data = client.getData(ROOT_PATH + "/" + node, false, null);
                servers.add(new String(data));
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return servers;
    }
    private void getConnection(Watcher watcher) throws IOException {
        this.client = new ZooKeeper(connectString, sessionTimeOut, watcher);
    }
    /**
     * 服务元数据
     */
    public static class Metadata {
        public Metadata() {
        }
        public Metadata(String ip, int port) {
            this.ip = ip;
            this.port = port;
        }
        private String ip;
        private int port;
        public String getIp() {
            return ip;
        }
        public void setIp(String ip) {
            this.ip = ip;
        }
        public int getPort() {
            return port;
        }
        public void setPort(int port) {
            this.port = port;
        }
        @Override
        public String toString() {
            return "{" + "ip='" + ip + '\'' + ", port=" + port + '}';
        }
    }
}

该类中两个核心方法:doRegister()subServers() 服务注册和订阅。

  • doRegister()主要是往 Zookeeper 中创建了一个临时节点数据,临时节点的优势就是当服务出现断电等异常停机时,节点会自动删除。
  • subServers()则是去 Zookeeper 读取了所有服务提供者的信息,并且监听了节点状态,当节点发生创建、删除、更新等事件时重新获取服务者的信息,做到数据实时更新。

至此,一个简单的注册中心就完成了,当然,如果要实现一个成熟的注册中心,还要考虑负载均衡、高可用性和容错、服务治理和路由控制等功能,这里先不展开。

服务注册

当有了注册中心,服务提供者就可以调用 doRegister() 进行注册,代码如下:

public class ProviderServer {
    public static void main(String[] args) throws Exception {
        RegistrationCenter registrationCenter = new RegistrationCenter();
        registrationCenter.doRegister("provider", new RegistrationCenter.Metadata("127.0.0.1", 8080));
        Thread.sleep(Long.MAX_VALUE);
    }
}

服务发现

同样,服务消费者可以调用 subServers() 发现服务提供者,同时当服务提供者发生变化时会通知到消费者。代码如下:

public class ConsumerServer {
    public static void main(String[] args) throws Exception {
        RegistrationCenter registrationCenter = new RegistrationCenter(newServers -> {
            System.out.println("服务更新了..."+newServers);
        });
        List<String> servers = registrationCenter.subServers();
        System.out.println(servers);
        Thread.sleep(Long.MAX_VALUE);
    }
}

总结

服务注册和服务发现功能是为了解决分布式系统中的服务管理和通信问题而设计的,经过不断的发展与负载均衡、健康监测、服务治理和路由控制等功能完善成为一个注册中心。服务注册和服务发现有助于实现系统的弹性和可扩展性,因为新的服务实例可以动态地加入系统,而无需手动配置和修改已有的代码。

以上就是基于Zookeeper实现服务注册和服务发现功能的详细内容,更多关于Zookeeper服务注册和发现的资料请关注脚本之家其它相关文章!

相关文章

  • Mybatis懒加载的实现

    Mybatis懒加载的实现

    这篇文章主要介绍了Mybatis懒加载的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • SpringBoot模拟员工数据库并实现增删改查操作

    SpringBoot模拟员工数据库并实现增删改查操作

    这篇文章主要给大家介绍了关于SpringBoot模拟员工数据库并实现增删改查操作的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-09-09
  • jvm垃圾回收之GC调优工具分析详解

    jvm垃圾回收之GC调优工具分析详解

    这篇文章主要为大家介绍了jvm垃圾回收之GC调优工具的分析详解,在进行JVM GC性能调优之前,需要使用某些工具获取到当前应用的状态信息
    2022-01-01
  • java图形界面之加法计算器

    java图形界面之加法计算器

    这篇文章主要为大家详细介绍了java图形界面之加法计算器,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • 深入了解final在java中的应用

    深入了解final在java中的应用

    谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字。另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法。
    2019-06-06
  • Mybatis优化检索的方法详解

    Mybatis优化检索的方法详解

    MyBatis是一款优秀的基于Java的持久层框架,它可以将 SQL 语句和数据库中的记录映射成为 Java 对象,并且支持灵活的 SQL 查询语句,在Mybatis中,可以使用动态SQL来灵活构造SQL语句,从而满足各种不同的检索需求,本文介绍Mybatis如何优化检索,需要的朋友可以参考下
    2024-05-05
  • 图解分析Javaweb进程与线程

    图解分析Javaweb进程与线程

    这篇文章主要介绍了Javaweb进程与线程的知识,本篇文章通过简要的案例,讲解了它的基础原理与使用,以下就是详细内容,需要的朋友可以参考下
    2022-03-03
  • Java常见的数据结构之栈和队列详解

    Java常见的数据结构之栈和队列详解

    这篇文章主要介绍了Java常见的数据结构之栈和队列详解,栈(Stack) 是一种基本的数据结构,具有后进先出(LIFO)的特性,类似于现实生活中的一叠盘子,栈用于存储一组元素,但只允许在栈顶进行插入(入栈)和删除(出栈)操作,需要的朋友可以参考下
    2023-10-10
  • java基于mongodb实现分布式锁的示例代码

    java基于mongodb实现分布式锁的示例代码

    本文主要介绍了java基于mongodb实现分布式锁,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • Java8日期时间类LocalDateTime比较大小举例

    Java8日期时间类LocalDateTime比较大小举例

    LocalDate是Java 8中的日期类之一,它表示一个日期,下面这篇文章主要给大家介绍了关于Java8日期时间类LocalDateTime比较大小的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-05-05

最新评论