用ZooKeeper实现分布式配置中心 微服务注册发现

ZooKeeper集群主要是帮我们做分布式协调的,今天我们用zk实现分布式配置

引入zk jar包

<dependency>
	<groupId>org.apache.zookeeper</groupId>
	<artifactId>zookeeper</artifactId>
	<version>3.6.2</version>
</dependency>

这里zk的jar包版本一定要和你自己zk server的版本一致。

在这里插入图片描述

分布式配置中心

为什么需要做分布式配置呢?

比如我们线上有很多微服务分布在很多服务器上,有一天其中一个微服务比如user-serviceip地址需要变更,而user-service需要对很多其他程序提供服务,这个时候如果没有一个统一协调的东西,每个用到user-service的应用程序都要做相应的ip地址修改,这将是一件很麻烦的事情!

使用zk来做分布式的配置,能够解决这个问题。

在这里插入图片描述

下面演示一下如何code用zk做分布式配置。

准备一个配置类

既然做分布式配置,我们先模拟构造一个配置出来,这个配置就是用来同步user-service的ip地址用的:

public class MyConfig {

    private String userServiceIP;

    public String getUserServiceIP() {
        return userServiceIP;
    }

    public void setUserServiceIP(String userServiceIP) {
        this.userServiceIP = userServiceIP;
    }
}

获取zk

@Public
public class ZooKeeper implements AutoCloseable

ZooKeeper类是ZooKeeper客户端库的主要类。要使用ZooKeeper服务,应用程序必须首先实例化ZooKeeper类的对象。所有的操作都将通过调用ZooKeeper类的方法来完成。这个类的方法是线程安全的,除非另有说明。

常用的构造方法:

ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)

要创建一个ZooKeeper客户端对象,应用程序需要传递一个连接字符串,该字符串包含一个由逗号分隔的主机:端口对列表,每个端口对对应于一个ZooKeeper服务器。

String servers = "192.168.134.136:2181,192.168.134.137:2181,192.168.134.140:2181,192.168.134.141:2181/conf";

ZooKeeper zk = new ZooKeeper(servers, 3000, new Watcher{...});

这里提一下,192.168.134.141:2181/conf这个后面呆了个/conf,表示客户端如果成功建立了到zk集群的连接,那么默认该客户端工作的根path就是/conf,如果不带/conf,默认根path是/

使用zk

一旦建立了到服务器的连接,会话ID就被分配给客户端。客户端将定期向服务器发送心跳,以保持会话有效。

只要客户机的会话ID保持有效,应用程序就可以通过客户机调用ZooKeeper api。

如果由于某种原因,客户端在很长一段时间内(例如超过sessionTimeout值)没有向服务器发送心跳,服务器将使会话过期,并且会话ID将失效。客户端对象将不再可用。此时要进行ZooKeeper API调用,应用程序必须创建一个新的客户端对象。

如果客户端当前连接的ZooKeeper服务器失败或没有响应,客户端将在其会话ID到期前自动尝试连接到另一个服务器。如果成功,应用程序可以继续使用客户机。

ZooKeeper API方法可以是同步的,也可以是异步的。同步方法会阻塞,直到服务器响应为止。异步方法只是将请求放入队列以便立即发送和返回。它们接受一个回调对象,该对象将在请求成功执行或出现错误时执行,并带有一个指示错误的适当返回代码(rc)。

常用的API
  • exists
//返回给定路径的节点的stat。如果不存在这样的节点,则返回null。
//如果监视为真,并且调用成功(没有抛出异常),则在节点上保留一个具有给定路径的监视。
//当创建/删除节点或设置节点上的数据的操作成功时,将触发监视。
public Stat exists(String path,
                   boolean watch)
            throws KeeperException,
                   InterruptedException
//exists的异步实现版本
public void exists(String path,
                   Watcher watcher,
                   AsyncCallback.StatCallback cb,
                   Object ctx)
//也是exists的异步实现版本
public void exists(String path,
                   boolean watch,
                   AsyncCallback.StatCallback cb,
                   Object ctx)

一般情况下,我们会使用异步版本,需要自定义一个回调函数,如果传参Watcher watcher,需要实现Watcher接口。

  • getData
//返回给定路径的节点的数据和stat。
//如果监视是非空的,并且调用成功(没有抛出异常),则节点上将保留一个带有给定路径的监视。
//在节点上设置数据或删除节点的操作成功后,将触发监视。
public byte[] getData(String path,
                      Watcher watcher,
                      Stat stat)
               throws KeeperException,
                      InterruptedException

//返回给定路径的节点的数据和stat。
//如果监视为真,并且调用成功(没有抛出异常),则在节点上保留一个具有给定路径的监视。
//在节点上设置数据或删除节点的操作成功后,将触发监视。                     
public byte[] getData(String path,
                      boolean watch,
                      Stat stat)
               throws KeeperException,
                      InterruptedException
//getData的异步实现版本。
public void getData(String path,
                    Watcher watcher,
                    AsyncCallback.DataCallback cb,
                    Object ctx)

public void getData(String path,
                    boolean watch,
                    AsyncCallback.DataCallback cb,
                    Object ctx)

代码实现

基于以上API介绍,我们来实现一个简单的分布式配置。

创建zk对象的时候,需要一个watcher,后面进行node操作也需要watcher,但是这两类watcher的功能不一样,创建zk对象需要的watcher我定义成DefaultWatcher:

public class DefaultWatcher implements Watcher {

    private CountDownLatch latch;

    public DefaultWatcher() {
    }

    public DefaultWatcher(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        System.out.println("DefaultWatcher watchedEvent:" + watchedEvent);
        Event.KeeperState state = watchedEvent.getState();
        switch (state) {
            case Disconnected:
                break;
            case SyncConnected:
                System.out.println("SyncConnected.");
                //连接成功后,执行countDown,此时便可以拿zk对象使用了
                latch.countDown();
                break;
            case AuthFailed:
                break;
            case ConnectedReadOnly:
                break;
            case SaslAuthenticated:
                break;
            case Expired:
                break;
            case Closed:
                break;
        }
    }
}

由于是异步调用,如果直接new完ZooKeeper对象后,不进行阻塞,很有可能还没连接上就执行后续的操作了,因此,此处我用CountDownLatch来做一个阻塞,当监测到连接成功了,就countDown,放行,执行后续zk操作。

zk连接成功后,我通过exists方法判断有没有/conf/userServiceConf这个node,如果有就getData

从前面的介绍,已经知道了exists和getData都需要一个callback,而且需要一个watcher,因此我可以构造一个WatchAndCallback类出来,该类实现Watcher以及节点状态的StatCallback和数据的DataCallback

public class WatchAndCallback implements Watcher, AsyncCallback.StatCallback, AsyncCallback.DataCallback {

    private ZooKeeper zk;
    private MyConfig config;
    CountDownLatch latch = new CountDownLatch(1);

    public void await() {
        zk.exists("/userServiceConf", this, this, "exists watch");
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    /**
     * DataCallback
     * 此回调用于检索节点的数据和stat
     * @param rc 调用返回的code或结果
     * @param path 传递给异步调用的路径
     * @param ctx 传递给异步调用的上下文对象
     * @param data 节点的数据
     * @param stat 指定节点的Stat对象
     * @author 行百里者
     * @create 2020/9/16 10:42
     **/
    @Override
    public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
        if (data != null) {
            String res = new String(data);
            config.setUserServiceIP(res);
            latch.countDown();
        }
    }

    /**
     * StatCallback
     * 此回调用于检索节点的stat
     * @param rc 调用返回的code或结果
     * @param path 传递给异步调用的路径
     * @param ctx 传递给异步调用的上下文对象
     * @param stat 指定路径上节点的Stat对象
     * @author 行百里者
     * @create 2020/9/16 10:39
     **/
    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {
        if (stat != null) {
            zk.getData("/userServiceConf", this, this, "Msg:Stat is not null");
        }
    }

    /**
     * Watcher接口的实现。
     * Watcher接口指定事件处理程序类必须实现的公共接口。
     * ZooKeeper客户机将从它连接到的ZooKeeper服务器获取各种事件。
     * 使用这种客户机的应用程序通过向客户机注册回调对象来处理这些事件。
     * 回调对象应该是实现监视器接口的类的实例。
     * @param watchedEvent WatchedEvent表示监视者能够响应的ZooKeeper上的更改。WatchedEvent包含发生了什么,ZooKeeper的当前状态,以及事件中涉及的znode的路径。
     * @author 行百里者
     * @create 2020/9/16 10:45
     **/
    @Override
    public void process(WatchedEvent watchedEvent) {
        Event.EventType type = watchedEvent.getType();
        switch (type) {
            case None:
                break;
            case NodeCreated:
                //当一个node被创建后,获取node
                //getData中又会触发StatCallback的回调processResult
                zk.getData("/userServiceConf", this, this, "Msg:NodeCreated");
                break;
            case NodeDeleted:
                //节点被删除
                config.setUserServiceIP("");
                //重新开启CountDownLatch
                latch = new CountDownLatch(1);
                break;
            case NodeDataChanged:
                //节点数据被改变了
                //会触发DataCallback的回调
                zk.getData("/userServiceConf", this, this, "Msg:NodeDataChanged");
                break;
            case NodeChildrenChanged:
                break;
            case DataWatchRemoved:
                break;
            case ChildWatchRemoved:
                break;
            case PersistentWatchRemoved:
                break;
        }
    }

    public ZooKeeper getZk() {
        return zk;
    }

    public void setZk(ZooKeeper zk) {
        this.zk = zk;
    }

    public MyConfig getConfig() {
        return config;
    }

    public void setConfig(MyConfig config) {
        this.config = config;
    }
}

前面都准备好了,可以编写测试了:

public class ZooKeeperConfigTest {
    static String servers = "192.168.134.136:2181,192.168.134.137:2181,192.168.134.140:2181,192.168.134.141:2181/conf";

    static ZooKeeper zk;

    static CountDownLatch latch = new CountDownLatch(1);

    @SneakyThrows
    @BeforeAll
    public static void getZK() {
        //System.out.println("servers:" + servers);
        //因为是异步的,所以要await,等到连接上zk集群之后再进行后续操作
        zk = new ZooKeeper(servers, 3000, new DefaultWatcher(latch));
        latch.await();
    }

    @SneakyThrows
    @AfterAll
    public static void close() {
        zk.close();
    }

    @Test
    public void zkConfigTest() {
        WatchAndCallback watchAndCallback = new WatchAndCallback();
        MyConfig config = new MyConfig();
        watchAndCallback.setConfig(config);
        watchAndCallback.setZk(zk);

        //阻塞等待
        watchAndCallback.await();

        //方便观测,写个死循环
        for(;;) {
            if (config.getUserServiceIP().equals("")) {
                System.out.println("node可能被删除了");
            } else {
                System.out.println("userServiceIP:" + config.getUserServiceIP());
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

运行验证

集群初始状态

[zk: localhost:2181(CONNECTED) 0] ls /
[laogong, zookeeper]

可以看到,这时还没有 /conf/userServiceConf这个节点,此时启动测试程序:

在这里插入图片描述

ok,连接上了。我在zknode1这个zkserver上开一个zkCli,手动创建/conf/userServiceConf并赋值 192.0.0.1:8080,同时观察程序运行情况:

在这里插入图片描述

可以看到,创建完成之后,程序马上给出响应,打印出了我配置的值userServiceIP:192.0.0.1:8080

此时,再设置/conf/userServiceConf的值为 192.0.0.2:9080

在这里插入图片描述

客户端很快得到了新值。

此时我要删除了/conf/userServiceConf这个节点,会发生什么?前面程序已经写了watch,当检测到node被删除,watch and callback执行:

case NodeDeleted:
    //节点被删除
    config.setUserServiceIP("");
    //重新开启CountDownLatch
    latch = new CountDownLatch(1);
    break;

在测试类:

if (config.getUserServiceIP().equals("")) {
    System.out.println("node可能被删除了");
    //此时应该阻塞住,等待着node重新创建
    watchAndCallback.await();
}

实操删除node:

在这里插入图片描述

执行了System.out.println("node可能被删除了");并且程序阻塞在此,此时重新创建/conf/userServiceConf节点:

在这里插入图片描述

程序正常运行,并且成功获取到了zk配置的最新数据!

OK,至此已经体验了zk能够实现分布式配置中心这个功能了。

案例中我是用的是getData,实际场景我们可能更多地用getChildren,获取一系列服务的ip。

小结

前面实现的案例,可以说是分布式配置中心,也可以说是微服务的注册发现

其实zookeeper的高可用、快速、一致性等等非常优秀的特点能完美的实现分布式配置中心,也能用它来实现微服务的注册发现。

本文代码地址:

https://github.com/traveler100/JavaJourney/tree/master/code/demo/zookeeper-demo

如果对你有帮助,请帮忙star,感谢!

推荐阅读

看完点赞,养成习惯。

举手之劳,赞有余香。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值