三天学会ZooKeeper第二天(全网最细)

ZooKeeper

  • Zookeeper session 基本原理
  • zookeeper 事件监听机制
  • Zookeeper watcher 事件机制原理剖析
  • ZooKeeper应用场景案例
  • zookeeper 集群搭建
  • 一致性协议:zab协议
  • zookeeper的leader选举
  • observer角色及其配置
  • zookeeperAPI连接集群

本篇文章参考黑马程序员ZooKeeper教程

三天学会ZooKeeper第一天(全网最细)
三天学会ZooKeeper第三天(全网最细)

Zookeeper session 基本原理

客户端与服务端之间的连接是基于 TCP 长连接,client 端连接 server 端默认的 2181 端口,也就是 session 会话。从第一次连接建立开始,客户端开始会话的生命周期,客户端向服务端的ping包请求,每个会话都可以设置一个超时时间。

Session 的创建
sessionID: 会话ID,用来唯一标识一个会话,每次客户端创建会话的时候,zookeeper 都会为其分配一个全局唯一的 sessionID。zookeeper 创建 sessionID SessionTrackerImpl 中的源码。
三天学会ZooKeeper第二天(全网最细)_第1张图片
Timeout:会话超时时间。客户端在构造 Zookeeper 实例时候,向服务端发送配置的超时时间,server 端会根据自己的超时时间限制最终确认会话的超时时间。

TickTime:下次会话超时时间点,默认 2000 毫秒。可在 zoo.cfg 配置文件中配置,便于 server 端对 session 会话实行分桶策略管理。

isClosing:该属性标记一个会话是否已经被关闭,当 server 端检测到会话已经超时失效,该会话标记为"已关闭",不再处理该会话的新请求。

Session 的状态

connecting:连接中,session 一旦建立,状态就是 connecting 状态,时间很短。

connected:已连接,连接成功之后的状态。

closed:已关闭,发生在 session 过期,一般由于网络故障客户端重连失败,服务器宕机或者客户端主动断开。

会话超时管理(分桶策略+会话激活)

zookeeper 的 leader 服务器再运行期间定时进行会话超时检查,时间间隔是 ExpirationInterval,单位是毫秒,默认值是 tickTime,每隔 tickTime 进行一次会话超时检查。
三天学会ZooKeeper第二天(全网最细)_第2张图片
ExpirationTime 的计算方式:

ExpirationTime = CurrentTime + SessionTimeout;
ExpirationTime = (ExpirationTime / ExpirationInterval + 1) * ExpirationInterval;

在 zookeeper 运行过程中,客户端会在会话超时过期范围内向服务器发送请求(包括读和写)或者 ping 请求,俗称心跳检测完成会话激活,从而来保持会话的有效性。

会话激活流程:
三天学会ZooKeeper第二天(全网最细)_第3张图片
激活后进行迁移会话的过程,然后开始新一轮:
三天学会ZooKeeper第二天(全网最细)_第4张图片

zookeeper 事件监听机制

watcher概念

zookeeper提供了数据的发布/订阅功能,多个订阅者可同时监听某一特定主题对象,当该主题对象的自身状态发生变化时(例如节点内容改变、节点下的子节点列表改变等),会实时、主动通知所有订阅者zookeeper采用了Watcher机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时会异步通知客户端,因此客户端不必在Watcher注册后轮询阻塞,从而减轻了客户端压力。watcher机制实际上与观察者模式类似,也可看作是一种观察者模式在分布式场景下的实现方式。

watcher架构

  1. Zookeeper服务端
  2. Zookeeper客户端
  3. 客户端的ZKWatchManager对象
    三天学会ZooKeeper第二天(全网最细)_第5张图片
    客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。当ZooKeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑,从而完成整体的数据发布/订阅流程。

watcher特性
三天学会ZooKeeper第二天(全网最细)_第6张图片
watcher接口设计

Watcher是一个接口,任何实现了Watcher接口的类就是一个新的Watcher。
Watcher内部包含了两个枚举类:KeeperState、EventType
三天学会ZooKeeper第二天(全网最细)_第7张图片
Watcher通知状态(KeeperState)

KeeperState是客户端与服务端连接状态发生变化时对应的通知类型。路径为
org.apache.zookeeper.Watcher.Event.KeeperState,是一个枚举类,其枚举属性
如下:
三天学会ZooKeeper第二天(全网最细)_第8张图片
Watcher事件类型(EventType)

EventType是数据节点(znode)发生变化时对应的通知类型。EventType变化时
KeeperState永远处于SyncConnected通知状态下;当KeeperState发生变化时,EventType永远为None。其路径为org.apache.zookeeper.Watcher.Event.EventType,是一个枚举类,枚举属性如下:
三天学会ZooKeeper第二天(全网最细)_第9张图片
注:客户端接收到的相关事件通知中只包含状态及类型等信息,不包括节点变化前后的具体内容,变化前的数据需业务自身存储,变化后的数据需调用get等方法重新获取;

捕获相应的事件

下表以node-x节点为例,说明调用的注册方法和可监听事件间的关系:
三天学会ZooKeeper第二天(全网最细)_第10张图片

客服端与服务器的连接状态

KeeperState 通知状态
SyncConnected:客户端与服务器正常连接时
Disconnected:客户端与服务器断开连接时
Expired:会话session失效时
AuthFailed:身份认证失败时
事件类型为:None

public class ZKConnectionWatcher implements Watcher {
     
    private static String connectString = "192.168.226.144:2181";
    // 计数器对象
    static CountDownLatch countDownLatch = new CountDownLatch(1);
    // 连接对象
    static ZooKeeper zooKeeper;

    @Override
    public void process(WatchedEvent event) {
     
        try {
     
            // 事件类型
            if (event.getType() == Event.EventType.None) {
     
                if (event.getState() == Event.KeeperState.SyncConnected) {
     
                    System.out.println("连接创建成功!");
                    countDownLatch.countDown();
                } else if (event.getState() == Event.KeeperState.Disconnected) {
     
                    System.out.println("断开连接!");
                } else if (event.getState() == Event.KeeperState.Expired) {
     
                    System.out.println("会话超时!");
                    zooKeeper = new ZooKeeper(connectString, 5000, new ZKConnectionWatcher());
                } else if (event.getState() == Event.KeeperState.AuthFailed) {
     
                    System.out.println("认证失败!");
                }
            }
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }


    public static void main(String[] args) {
     
        try {
     
            zooKeeper = new ZooKeeper(connectString, 5000, new ZKConnectionWatcher());
            // 阻塞线程等待连接的创建
            countDownLatch.await();
            // 会话id
            System.out.println(zooKeeper.getSessionId());
            // 添加授权用户
            zooKeeper.addAuthInfo("digest", "zzn:1234561".getBytes());
            byte[] bs = zooKeeper.getData("/node1", false, null);
            System.out.println(new String(bs));
            Thread.sleep(50000);
            zooKeeper.close();
            System.out.println("结束");
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }
}

检查节点是否存在

// 使用连接对象的监视器
exists(String path, boolean b)
// 自定义监视器
exists(String path, Watcher w)
// NodeCreated:节点创建
// NodeDeleted:节点删除
// NodeDataChanged:节点内容发生变化

path- znode路径。
b- 是否使用连接对象中注册的监视器。
w-监视器对象。

public class ZKWatcherExists {
     
    public static String IP = "192.168.226.144:2181";
    ZooKeeper zooKeeper = null;

    @Before
    public void before() throws IOException, InterruptedException {
     
        CountDownLatch countDownLatch = new CountDownLatch(1);
        // 连接zookeeper客户端
        zooKeeper = new ZooKeeper(IP, 6000, event -> {
     
            System.out.println("连接对象的参数!");
            // 连接成功
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
     
                countDownLatch.countDown();
            }
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        });
        countDownLatch.await();
    }

    @After
    public void after() throws InterruptedException {
     
        zooKeeper.close();
    }

    @Test
    public void watcherExists1() throws KeeperException, InterruptedException {
     
        // arg1:节点的路径
        // arg2:使用连接对象中的watcher
        Stat exists = zooKeeper.exists("/watcher/node1", true);
        System.out.println(exists == null ? "节点不存在" : "节点存在");
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherExists2() throws KeeperException, InterruptedException {
     
        // arg1:节点的路径
        // arg2:自定义watcher对象
        zooKeeper.exists("/watcher/node1", event -> {
     
            System.out.println("自定义watcher");
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        });
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherExists3() throws KeeperException, InterruptedException {
     
        // watcher一次性
        Watcher watcher = new Watcher() {
     
            @Override
            public void process(WatchedEvent event) {
     
                try {
     
                    System.out.println("自定义watcher");
                    System.out.println("path=" + event.getPath());
                    System.out.println("eventType=" + event.getType());
                    zooKeeper.exists("/watcher/node1", this);
                } catch (Exception ex) {
     
                    ex.printStackTrace();
                }
            }
        };
        zooKeeper.exists("/watcher/node1", watcher);
        Thread.sleep(80000);
        System.out.println("结束");
    }

    @Test
    public void watcherExists4() throws KeeperException, InterruptedException {
     
        // 注册多个监听器对象
        zooKeeper.exists("/watcher/node1", event -> {
     
            System.out.println("1");
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        });
        zooKeeper.exists("/watcher/node1", event -> {
     
            System.out.println("2");
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        });
        Thread.sleep(80000);
        System.out.println("结束");
    }
}

查看节点

// 使用连接对象的监视器
getData(String path, boolean b, Stat stat)
// 自定义监视器
getData(String path, Watcher w, Stat stat)
// NodeDeleted:节点删除
// NodeDataChanged:节点内容发生变化

path- znode路径。
b- 是否使用连接对象中注册的监视器。
w-监视器对象。
stat- 返回znode的元数据。

/**
 * getData 监控 Change ,Deleted
 */
public class ZKWatcherGetData {
     
    public static String IP = "192.168.226.144:2181";
    ZooKeeper zooKeeper = null;

    @Before
    public void before() throws IOException, InterruptedException {
     
        CountDownLatch countDownLatch = new CountDownLatch(1);
        // 连接zookeeper客户端
        zooKeeper = new ZooKeeper(IP, 6000, event -> {
     
            System.out.println("连接对象的参数!");
            // 连接成功
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
     
                countDownLatch.countDown();
            }
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        });
        countDownLatch.await();
    }

    @After
    public void after() throws InterruptedException {
     
        zooKeeper.close();
    }

    @Test
    public void watcherGetData1() throws KeeperException, InterruptedException {
     
        // arg1:节点的路径
        // arg2:使用连接对象中的watcher
        zooKeeper.getData("/watcher/node2", true, null);
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherGetData2() throws KeeperException, InterruptedException {
     
        // arg1:节点的路径
        // arg2:自定义watcher对象
        zooKeeper.getData("/watcher/node2", event -> {
     
            System.out.println("自定义watcher");
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        }, null);
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherGetData3() throws KeeperException, InterruptedException {
     
        // 一次性
        Watcher watcher = new Watcher() {
     
            @Override
            public void process(WatchedEvent event) {
     
                try {
     
                    System.out.println("自定义watcher");
                    System.out.println("path=" + event.getPath());
                    System.out.println("eventType=" + event.getType());
                    if (event.getType() == Event.EventType.NodeDataChanged) {
     
                        zooKeeper.getData("/watcher/node2", this, null);
                    }
                } catch (Exception ex) {
     
                    ex.printStackTrace();
                }
            }
        };
        byte[] data = zooKeeper.getData("/watcher/node2", watcher, null);
        System.out.println("data = " + new String(data));
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherGetData4() throws KeeperException, InterruptedException {
     
        // 注册多个监听器对象
        zooKeeper.getData("/watcher/node2", new Watcher() {
     
            @Override
            public void process(WatchedEvent event) {
     
                try {
     
                    System.out.println("1");
                    System.out.println("path=" + event.getPath());
                    System.out.println("eventType=" + event.getType());
                    if (event.getType() == Event.EventType.NodeDataChanged) {
     
                        zooKeeper.getData("/watcher/node2", this, null);
                    }
                } catch (Exception ex) {
     
                    ex.printStackTrace();
                }
            }
        }, null);
        zooKeeper.getData("/watcher/node2", new Watcher() {
     
            @Override
            public void process(WatchedEvent event) {
     
                try {
     
                    System.out.println("2");
                    System.out.println("path=" + event.getPath());
                    System.out.println("eventType=" + event.getType());
                    if (event.getType() == Event.EventType.NodeDataChanged) {
     
                        zooKeeper.getData("/watcher/node2", this, null);
                    }
                } catch (Exception ex) {
     
                    ex.printStackTrace();
                }
            }
        }, null);
        Thread.sleep(50000);
        System.out.println("结束");
    }
}

查看子节点

// 使用连接对象的监视器
getChildren(String path, boolean b)
// 自定义监视器
getChildren(String path, Watcher w)
// NodeChildrenChanged:子节点发生变化
// NodeDeleted:节点删除

path- znode路径。
b- 是否使用连接对象中注册的监视器。
w-监视器对象。


/**
 * 监控子节点的创建和删除
 */
public class ZKWatcherGetChild {
     
    public static String IP = "192.168.226.144:2181";
    ZooKeeper zooKeeper = null;

    @Before
    public void before() throws IOException, InterruptedException {
     
        CountDownLatch connectedSemaphore = new CountDownLatch(1);
        // 连接zookeeper客户端
        zooKeeper = new ZooKeeper(IP, 6000, event -> {
     
            System.out.println("连接对象的参数!");
            // 连接成功
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
     
                connectedSemaphore.countDown();
            }
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        });
        connectedSemaphore.await();
    }

    @After
    public void after() throws InterruptedException {
     
        zooKeeper.close();
    }

    @Test
    public void watcherGetChild1() throws KeeperException, InterruptedException {
     
        // arg1:节点的路径
        // arg2:使用连接对象中的 watcher
        zooKeeper.getChildren("/watcher", true);
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherGetChild2() throws KeeperException, InterruptedException {
     
        // arg1:节点的路径
        // arg2:自定义watcher
        zooKeeper.getChildren("/watcher", event -> {
     
            System.out.println("自定义watcher");
            System.out.println("path=" + event.getPath());
            System.out.println("eventType=" + event.getType());
        });
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherGetChild3() throws KeeperException, InterruptedException {
     
        // 一次性
        Watcher watcher = new Watcher() {
     
            @Override
            public void process(WatchedEvent event) {
     
                try {
     
                    System.out.println("自定义watcher");
                    System.out.println("path=" + event.getPath());
                    System.out.println("eventType=" + event.getType());
                    if (event.getType() ==
                            Event.EventType.NodeChildrenChanged) {
     
                        zooKeeper.getChildren("/watcher", this);
                    }
                } catch (Exception ex) {
     
                    ex.printStackTrace();
                }
            }
        };
        zooKeeper.getChildren("/watcher", watcher);
        Thread.sleep(50000);
        System.out.println("结束");
    }

    @Test
    public void watcherGetChild4() throws KeeperException, InterruptedException {
     
        // 多个监视器对象
        zooKeeper.getChildren("/watcher", new Watcher() {
     
            @Override
            public void process(WatchedEvent event) {
     
                try {
     
                    System.out.println("1");
                    System.out.println("path=" + event.getPath());
                    System.out.println("eventType=" + event.getType());
                    if (event.getType() == Event.EventType.NodeChildrenChanged) {
     
                        zooKeeper.getChildren("/watcher", this);
                    }
                } catch (Exception ex) {
     
                    ex.printStackTrace();
                }
            }
        });
        zooKeeper.getChildren("/watcher", new Watcher() {
     
            @Override
            public void process(WatchedEvent event) {
     
                try {
     
                    System.out.println("2");
                    System.out.println("path=" + event.getPath());
                    System.out.println("eventType=" + event.getType());
                    if (event.getType() == Event.EventType.NodeChildrenChanged) {
     
                        zooKeeper.getChildren("/watcher", this);
                    }
                } catch (Exception ex) {
     
                    ex.printStackTrace();
                }
            }
        });
        Thread.sleep(50000);
        System.out.println("结束");
    }
}

Zookeeper watcher 事件机制原理剖析

zookeeper 的 watcher 机制,可以分为四个过程:

客户端注册 watcher。
服务端处理 watcher。
服务端触发 watcher 事件。
客户端回调 watcher。

以 exists 方法举例说明其原理

public class WatcherDemo implements Watcher {
     
    static ZooKeeper zooKeeper;
    static {
     
        try {
     
            zooKeeper = new ZooKeeper("192.168.3.39:2181", 4000,new WatcherDemo());
        } catch (IOException e) {
     
            e.printStackTrace();
        }
    }
    @Override
    public void process(WatchedEvent event) {
     
        System.out.println("eventType:"+event.getType());
        if(event.getType()==Event.EventType.NodeDataChanged){
     
            try {
     
                zooKeeper.exists(event.getPath(),true);
            } catch (KeeperException e) {
     
                e.printStackTrace();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
     
        String path="/watcher";
        if(zooKeeper.exists(path,false)==null) {
     
            zooKeeper.create("/watcher", "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        Thread.sleep(1000);
        System.out.println("-----------");
        //true表示使用zookeeper实例中配置的watcher
        Stat stat=zooKeeper.exists(path,true);
        System.in.read();
    }
}

三天学会ZooKeeper第二天(全网最细)_第11张图片
此时启动 zookeeper 命令行终端,查看并且删除 watcher 节点:
三天学会ZooKeeper第二天(全网最细)_第12张图片
IDE 控制台输出,触发了节点删除事件:
三天学会ZooKeeper第二天(全网最细)_第13张图片
客户端发送请求给服务端是通过 TCP 长连接建立网络通道,底层默认是通过 java 的 NIO 方式,也可以配置 netty 实现方式。
三天学会ZooKeeper第二天(全网最细)_第14张图片
注册 watcher 监听事件流程图:
三天学会ZooKeeper第二天(全网最细)_第15张图片
1、客户端发送事件通知请求
在 Zookeeper 类调用 exists 方法时候,把创建事件监听封装到 request 对象中,watch 属性设置为 true,待服务端返回 response 后把监听事件封装到客户端的 ZKWatchManager 类中。
三天学会ZooKeeper第二天(全网最细)_第16张图片
2、服务端处理 watcher 事件的请求
服务端 NIOServerCnxn 类用来处理客户端发送过来的请求,最终调用到 FinalRequestProcessor,其中有一段源码添加客户端发送过来的 watcher 事件:
三天学会ZooKeeper第二天(全网最细)_第17张图片
然后进入 statNode 方法,在 DataTree 类方法中添加 watcher 事件,并保存至 WatchManager 的 watchTable 与 watchTable 中。
三天学会ZooKeeper第二天(全网最细)_第18张图片
三天学会ZooKeeper第二天(全网最细)_第19张图片
3、服务端触发 watcher 事件流程:
若服务端某个被监听的节点发生事务请求,服务端处理请求过程中调用 FinalRequestProcessor 类 processRequest 方法中的代码如下所示:
三天学会ZooKeeper第二天(全网最细)_第20张图片
删除调用链最终到 DataTree 类中删除节点分支的触发代码段:
三天学会ZooKeeper第二天(全网最细)_第21张图片
进入 WatchManager 类的 triggerWatch 方法:
三天学会ZooKeeper第二天(全网最细)_第22张图片
继续跟踪进入 NIOServerCnxn,构建了一个 xid 为 -1,zxid 为 -1 的 ReplyHeader 对象,然后再调用 sendResonpe 方法。
三天学会ZooKeeper第二天(全网最细)_第23张图片
4、客户端回调 watcher 事件
客户端 SendThread 类 readResponse 方法接收服务端触发的事件通知,进入 xid 为 -1 流程,处理 Event 事件。
三天学会ZooKeeper第二天(全网最细)_第24张图片

ZooKeeper应用场景案例

配置中心案例

工作中有这样的一个场景: 数据库用户名和密码信息放在一个配置文件中,应用读取该配置文件,配置文件信息放入缓存。若数据库的用户名和密码改变时候,还需要重新加载缓存,比较麻烦,通过ZooKeeper可以轻松完成,当数据库发生变化时自动完成缓存同步。

思路分析:

  1. 连接zookeeper服务器
  2. 读取zookeeper中的配置信息,注册watcher监听器,存入本地变量
  3. 当zookeeper中的配置信息发生变化时,通过watcher的回调方法捕获数据变化事件
  4. 重新获取配置信息
public class MyConfigCenter implements Watcher {
     

    // zk的连接串
    public static String IP = "192.168.226.144:2181";

    // 计数器对象
    CountDownLatch countDownLatch = new CountDownLatch(1);
    // 连接对象
    static ZooKeeper zooKeeper;

    // 用于本地化存储配置信息
    private String url;
    private String username;
    private String password;

    @Override
    public void process(WatchedEvent event) {
     
        try {
     
            // 捕获事件状态
            if (event.getType() == Event.EventType.None) {
     
                if (event.getState() == Event.KeeperState.SyncConnected) {
     
                    System.out.println("连接成功");
                    countDownLatch.countDown();
                } else if (event.getState() == Event.KeeperState.Disconnected) {
     
                    System.out.println("连接断开!");
                } else if (event.getState() == Event.KeeperState.Expired) {
     
                    System.out.println("连接超时!");
                    // 超时后服务器端已经将连接释放,需要重新连接服务器端
                    zooKeeper = new ZooKeeper(IP, 6000, new ZKConnectionWatcher());
                } else if (event.getState() == Event.KeeperState.AuthFailed) {
     
                    System.out.println("验证失败!");
                }
                // 当配置信息发生变化时
            } else if (event.getType() == EventType.NodeDataChanged) {
     
                initValue();
            }
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }

    // 构造方法
    public MyConfigCenter() throws IOException, InterruptedException {
     
        // 创建连接对象
        zooKeeper = new ZooKeeper(IP, 5000, this);
        // 阻塞线程,等待连接的创建成功
        countDownLatch.await();
        initValue();
    }

    // 连接zookeeper服务器,读取配置信息
    public void initValue() {
     
        try {
     
            // 读取配置信息
            this.url = new String(zooKeeper.getData("/config/url", true, null));
            this.username = new String(zooKeeper.getData("/config/username", true, null));
            this.password = new String(zooKeeper.getData("/config/password", true, null));
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
     
        try {
     
            MyConfigCenter myConfigCenter = new MyConfigCenter();
            for (int i = 1; i <= 20; i++) {
     
                Thread.sleep(5000);
                System.out.println("url:" + myConfigCenter.getUrl());
                System.out.println("username:" + myConfigCenter.getUsername());
                System.out.println("password:" + myConfigCenter.getPassword());
                System.out.println("########################################");
            }
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }

    public String getUrl() {
     
        return url;
    }

    public void setUrl(String url) {
     
        this.url = url;
    }

    public String getUsername() {
     
        return username;
    }

    public void setUsername(String username) {
     
        this.username = username;
    }

    public String getPassword() {
     
        return password;
    }

    public void setPassword(String password) {
     
        this.password = password;
    }
}

生成分布式唯一ID

在过去的单库单表型系统中,通常可以使用数据库字段自带的auto_increment属性来自动为每条记录生成一个唯一的ID。但是分库分表后,就无法在依靠数据库的auto_increment属性来唯一标识一条记录了。此时我们就可以用zookeeper在分布式环境下生成全局唯一ID。

设计思路:

  1. 连接zookeeper服务器
  2. 指定路径生成临时有序节点
  3. 取序列号及为分布式环境下的唯一ID
public class GloballyUniqueId implements Watcher {
     
    // zk的连接串
    public static String IP = "192.168.226.144:2181";

    // 计数器对象
    CountDownLatch countDownLatch = new CountDownLatch(1);

    // 用户生成序号的节点
    String defaultPath = "/uniqueId";

    // 连接对象
    ZooKeeper zooKeeper;

    @Override
    public void process(WatchedEvent event) {
     
        try {
     
            // 捕获事件状态
            if (event.getType() == Watcher.Event.EventType.None) {
     
                if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
     
                    System.out.println("连接成功");
                    countDownLatch.countDown();
                } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
     
                    System.out.println("连接断开!");
                } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
     
                    System.out.println("连接超时!");
                    // 超时后服务器端已经将连接释放,需要重新连接服务器端
                    zooKeeper = new ZooKeeper(IP, 6000, new ZKConnectionWatcher());
                } else if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
     
                    System.out.println("验证失败!");
                }
            }
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }

    // 构造方法
    public GloballyUniqueId() {
     
        try {
     
            //打开连接
            zooKeeper = new ZooKeeper(IP, 5000, this);
            // 阻塞线程,等待连接的创建成功
            countDownLatch.await();
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }

    // 生成id的方法
    public String getUniqueId() {
     
        String path = "";
        try {
     
            //创建临时有序节点
            path = zooKeeper.create(defaultPath, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
        // /uniqueId0000000001
        return path.substring(9);
    }

    public static void main(String[] args) {
     
        GloballyUniqueId globallyUniqueId = new GloballyUniqueId();
        for (int i = 1; i <= 5; i++) {
     
            String id = globallyUniqueId.getUniqueId();
            System.out.println(id);
        }
    }
}

分布式锁
分布式锁有多种实现方式,比如通过数据库、redis都可实现。作为分布式协同工具ZooKeeper,当然也有着标准的实现方式。下面介绍在zookeeper中如何实现排他锁。

设计思路:

  1. 每个客户端往/Locks下创建临时有序节点/Locks/Lock000000001
  2. 客户端取得/Locks下子节点,并进行排序,判断排在最前面的是否为自己,如果自己的锁节点在第一位,代表获取锁成功
  3. 如果自己的锁节点不在第一位,则监听自己前一位的锁节点。例如,自己锁节点Lock 000000001
  4. 当前一位锁节点(Lock000000002)的逻辑
  5. 监听客户端重新执行第2步逻辑,判断自己是否获得了锁

/**
 * zookeeper 实现分布式锁
 */
public class MyLock {
     
    // zk的连接串
    public final String IP = "192.168.226.144:2181";

    // 计数器对象
    CountDownLatch countDownLatch = new CountDownLatch(1);

    //ZooKeeper配置信息
    ZooKeeper zooKeeper;
    private static final String LOCK_ROOT_PATH = "/Locks";
    private static final String LOCK_NODE_NAME = "Lock_";

    private String lockPath;

    // 打开zookeeper连接
    public MyLock() {
     
        try {
     
            zooKeeper = new ZooKeeper(IP, 5000, event -> {
     
                if (event.getType() == Watcher.Event.EventType.None) {
     
                    if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
     
                        System.out.println("连接成功!");
                        countDownLatch.countDown();
                    }
                }
            });
            countDownLatch.await();
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }

    //获取锁
    public void acquireLock() throws Exception {
     
        //创建锁节点
        createLock();
        //尝试获取锁
        attemptLock();
    }

    //创建锁节点
    private void createLock() throws Exception {
     
        //判断Locks是否存在,不存在创建
        Stat stat = zooKeeper.exists(LOCK_ROOT_PATH, false);
        if (stat == null) {
     
            zooKeeper.create(LOCK_ROOT_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        // 创建临时有序节点
        lockPath = zooKeeper.create(LOCK_ROOT_PATH + "/" + LOCK_NODE_NAME,
                new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println("节点创建成功:" + lockPath);
    }

    //监视器对象,监视上一个节点是否被删除
    final Watcher watcher = new Watcher() {
     
        @Override
        public void process(WatchedEvent event) {
     
            if (event.getType() == Event.EventType.NodeDeleted) {
     
                synchronized (this) {
     
                    notifyAll();
                }
            }
        }
    };

    //尝试获取锁
    private void attemptLock() throws Exception {
     
        // 获取Locks节点下的所有子节点
        List<String> list = zooKeeper.getChildren(LOCK_ROOT_PATH, false);
        // 对子节点进行排序
        Collections.sort(list);
        // /Locks/Lock_000000001
        int index = list.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));
        if (index == 0) {
     
            System.out.println("获取锁成功!");
        } else {
     
            // 上一个节点的路径
            String path = list.get(index - 1);
            Stat stat = zooKeeper.exists(LOCK_ROOT_PATH + "/" + path, watcher);
            if (stat != null) {
     
                synchronized (watcher) {
     
                    watcher.wait();
                }
            }
            attemptLock();
        }
    }

    //释放锁
    public void releaseLock() throws Exception {
     
        //删除临时有序节点
        zooKeeper.delete(this.lockPath, -1);
        zooKeeper.close();
        System.out.println("锁已经释放:" + this.lockPath);
    }

    public static void main(String[] args) {
     
        try {
     
            MyLock myLock = new MyLock();
            myLock.createLock();
            System.out.println("执行事务");
            myLock.releaseLock();
        } catch (Exception ex) {
     
            ex.printStackTrace();
        }
    }
}


/**
 * 模拟售票
 */
public class TicketSeller {
     
    private void sell() {
     
        System.out.println("售票开始");
        // 线程随机休眠数毫秒,模拟现实中的费时操作
        int sleepMillis = 5000;
        try {
     
            //代表复杂逻辑执行了一段时间
            Thread.sleep(sleepMillis);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("售票结束");
    }

    public void sellTicketWithLock() throws Exception {
     
        MyLock lock = new MyLock();
        // 获取锁
        lock.acquireLock();
        sell();
        //释放锁
        lock.releaseLock();
    }

    public static void main(String[] args) throws Exception {
     
        TicketSeller ticketSeller = new TicketSeller();
        for (int i = 0; i < 10; i++) {
     
            ticketSeller.sellTicketWithLock();
        }
    }
}

zookeeper 集群搭建

单机环境下,jdk、zookeeper 安装完毕,基于一台虚拟机,进行zookeeper伪集群搭建,zookeeper集群中包含3个节点,节点对外提供服务端口号分别为2181、2182、2183

  1. 基于zookeeper-3.4.10复制三份zookeeper安装好的服务器文件,目录名称分别为
    zookeeper2181、zookeeper2182、zookeeper2183
cp ‐r zookeeper‐3.4.10 zookeeper2181
cp ‐r zookeeper‐3.4.10 zookeeper2182
cp ‐r zookeeper‐3.4.10 zookeeper2183

三天学会ZooKeeper第二天(全网最细)_第25张图片

  1. 修改zookeeper2181服务器对应配置文件。
#服务器对应端口号
clientPort=2181
#数据快照文件所在路径
dataDir=/home/zookeeper/zookeeper2181/data
#集群配置信息
#server.A=B:C:D
#A:是一个数字,表示这个是服务器的编号
#B:是这个服务器的ip地址
#CZookeeper服务器之间的通信端口
#DLeader选举的端口
# 2287 端口号是节点之间通信的端口号 3387 是投票时用到的端口号
server.1=192.168.60.130:2287:3387
server.2=192.168.60.130:2288:3388
server.3=192.168.60.130:2289:3389

三天学会ZooKeeper第二天(全网最细)_第26张图片
3. 在 上一步 dataDir 指定的目录下,创建 myid 文件,然后在该文件添加上一步

#zookeeper2181对应的数字为1
#/home/zookeeper/zookeeper2181/data目录下执行命令
echo "1" > myid
  1. zookeeper2182、zookeeper2183参照步骤2/3进行相应配置
  2. 分别启动三台服务器,检验集群状态
./	zkCli.sh -server 192.168.60.130:2181
./	zkCli.sh -server 192.168.60.130:2182
./	zkCli.sh -server 192.168.60.130:2183

一致性协议:zab协议

zab协议 的全称是 Zookeeper Atomic Broadcast (zookeeper原子广播)。zookeeper 是通过 zab协议来保证分布式事务的最终一致性
基于zab协议,zookeeper集群中的角色主要有以下三类,如下表所示:
三天学会ZooKeeper第二天(全网最细)_第27张图片

zab广播模式工作原理,通过类似两阶段提交协议的方式解决数据一致性:
三天学会ZooKeeper第二天(全网最细)_第28张图片

  1. leader从客户端收到一个写请求
  2. leader生成一个新的事务并为这个事务生成一个唯一的ZXID
  3. leader将这个事务提议(propose)发送给所有的follows节点
  4. follower节点将收到的事务请求加入到历史队列(history queue)中,并发送ack给leader
  5. 当leader收到大多数follower(半数以上节点)的ack消息,leader会发送commit请求
  6. 当follower收到commit请求时,从历史队列中将事务请求commit

zookeeper的leader选举

服务器状态

looking:寻找leader状态。当服务器处于该状态时,它会认为当前集群中没有leader,因此需要进入leader选举状态。
leading: 领导者状态。表明当前服务器角色是leader。
following: 跟随者状态。表明当前服务器角色是follower。
observing:观察者状态。表明当前服务器角色是observer。

服务器启动时期的leader选举

在集群初始化阶段,当有一台服务器server1启动时,其单独无法进行和完成leader选举,当第二台服务器server2启动时,此时两台机器可以相互通信,每台机器都试图找到leader,于是进入leader选举过程。选举过程如下:

  1. 每个server发出一个投票。由于是初始情况,server1和server2都会将自己作为
    leader服务器来进行投票,每次投票会包含所推举的服务器的myid和zxid,使用
    (myid, zxid)来表示,此时server1的投票为(1, 0),server2的投票为(2, 0),然后各自
    将这个投票发给集群中其他机器。
  2. 集群中的每台服务器接收来自集群中各个服务器的投票。
  3. 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行pk,pk
    规则如下优先检查zxid。zxid比较大的服务器优先作为leader。如果zxid相同,那么就比较myid。myid较大的服务器作为leader服务器。对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的zxid,均为0,再比较myid,此时server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
  4. 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于server1、server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了leader
  5. 改变服务器状态。一旦确定了leader,每个服务器就会更新自己的状态,如果是
    follower,那么就变更为following,如果是leader,就变更为leading。

其选举流程如下图所示:

三天学会ZooKeeper第二天(全网最细)_第29张图片

服务器运行时期的Leader选举

在zookeeper运行期间,leader与非leader服务器各司其职,即便当有非leader服务器宕机或新加入,此时也不会影响leader,但是一旦leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮leader选举,其过程和启动时期的Leader选举过程基本一致。

假设正在运行的有server1、server2、server3三台服务器,当前leader是server2,若某一时刻leader挂了,此时便开始Leader选举。选举过程如下:

  1. 变更状态。leader挂后,余下的服务器都会将自己的服务器状态变更为looking,然后开始进入leader选举过程。
  2. 每个server会发出一个投票。在运行期间,每个服务器上的zxid可能不同,此时假定server1的zxid为122,server3的zxid为122,在第一轮投票中,server1和server3都会投自己,产生投票(1, 122),(3, 122),然后各自将投票发送给集群中所有机器。
  3. 接收来自各个服务器的投票。与启动时过程相同
  4. 处理投票。与启动时过程相同,此时,server3将会成为leader。5. 统计投票。与启动时过程相同。
  5. 改变服务器的状态。与启动时过程相同。

observer角色及其配置

observer角色特点: 不参与集群的leader选举 不参与集群中写数据时的ack反馈

为了使用observer角色,在任何想变成observer角色的配置文件中加入如下配置:
peerType=observer

并在所有server的配置文件中,配置成observer模式的server的那行配置追加:observer,例如:

server.3=192.168.60.130:2289:3389:observer

zookeeperAPI连接集群

ZooKeeper(String connectionString, int sessionTimeout, Watcher watcher)

connectionString - zooKeeper集合主机。
sessionTimeout - 会话超时(以毫秒为单位)。
watcher - 实现“监视器”界面的对象。ZooKeeper集合通过监视器对象返回连接状态。


/**
 * @author zzn
 * @since 2020/5/26 15:46
 */
public class ZookeeperConnection {
     
    // 单机
    // public static String connectString = "192.168.226.144:2181";
    // 集群
    public static String connectString = "192.168.226.144:2181,192.168.226.144:2182,192.168.226.144:2183";

    public static void main(String[] args) throws IOException, InterruptedException {
     
        // 计数器对象
        CountDownLatch countDownLatch = new CountDownLatch(1);

        // connectString: zookeeper 所在的服务器 ip 和端口
        // sessionTimeout: 客户端和服务器之间的会话超时时间 以毫秒为单位
        // watcher : 监视器对象
        ZooKeeper zooKeeper = new ZooKeeper(connectString, 5000, event -> {
     
            if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
     
                System.out.println("连接创建成功");
                countDownLatch.countDown();
            }
        });

        // 主线程阻塞等待连接对象的创建
        countDownLatch.await();
        // 会话编号
        System.out.println("会话编号" + zooKeeper.getSessionId());
        zooKeeper.close();
    }
}

你可能感兴趣的:(ZooKeeper,zookeeper,分布式)