zookeeper安装及使用(springboot)(单例&&集群)

前提准备

下载安装包

下载到指定文件夹并解压,如下

官网文档 

简介

ZooKeeper是一个用于分布式应用程序的分布式开源协调服务。它公开了一组简单的原语,分布式应用程序可以在这些原语的基础上实现更高级别的同步、配置维护、组和命名服务。它的设计易于编程,并使用了一个以熟悉的文件系统目录树结构为样式的数据模型。它在Java中运行,并具有Java和C的绑定。
众所周知,协调服务很难做好。他们特别容易出现诸如比赛条件和僵局之类的错误。ZooKeeper背后的动机是减轻分布式应用程序从头开始实现协调服务的责任。


ZooKeeper的设计目标之一是提供一个非常简单的编程接口。因此,它只支持以下操作:

  • create : 在树中的某个位置创建节点

  • delete : 删除节点

  • exists : 测试某个位置是否存在节点

  • get data : 从节点读取数据

  • set data : 将数据写入节点

  • get children : 检索节点的子级列表

  • sync : 等待数据传播

安装

单例模式

安装

下载并解压到指定文件夹,如下

要启动ZooKeeper,需要一个配置文件。以下是一个示例,请在conf/zoo.cfg中创建它 

tickTime=2000
dataDir=/home/zookeeper/zookeeper/data
clientPort=2181

tickTime:ZooKeeper使用的以毫秒为单位的基本时间单位。它用于进行心跳,最小会话超时将是tickTime的两倍。
dataDir:存储内存中数据库快照的位置,除非另有指定,否则存储数据库更新的事务日志。
clientPort:侦听客户端连接的端口

在conf目录下有zoo_sample.cfg,只需要复制一份改名为zoo.cfg,再修改其中的内容即可

启动zookeeper服务端

sh zkServer.sh start

启动zookeeper客户端 

sh zkCli.sh -server 127.0.0.1:2181

 客户端命令

通过help查看客户端命令

[zk: 127.0.0.1:2181(CONNECTED) 0] help
ZooKeeper -server host:port cmd args
        addauth scheme auth
        close
        config [-c] [-w] [-s]
        connect host:port
        create [-s] [-e] [-c] [-t ttl] path [data] [acl]
        delete [-v version] path
        deleteall path
        delquota [-n|-b] path
        get [-s] [-w] path
        getAcl [-s] path
        history
        listquota path
        ls [-s] [-w] [-R] path
        ls2 path [watch]
        printwatches on|off
        quit
        reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
        redo cmdno
        removewatches path [-c|-d|-a] [-l]
        rmr path
        set [-s] [-v version] path data
        setAcl [-s] [-v version] [-R] path acl
        setquota -n|-b val path
        stat [-w] path
        sync path
查看命令 

 ls [-s] [-w] [-R] path  查看path路径下面的文件  

  • -s:以大小(字节)的顺序显示子节点。
  • -w:以时间戳排序显示子节点。
  • -R:递归显示指定路径下的所有子节点。
[zk: 127.0.0.1:2181(CONNECTED) 1] ls /
[zookeeper]
创建节点命令 

create [-s] [-e] [-c] [-t ttl] path [data] [acl] 

  • -s:指定节点为顺序节点(Sequential Node),即在节点名称后会自动追加一个递增的序号。
  • -e:创建临时节点(Ephemeral Node),该节点会在客户端断开连接时被删除。
  • -c:创建容器节点(Container Node),该节点可包含子节点。
  • -t ttl:设置节点的 TTL(Time-To-Live),即节点的生存时间,单位为毫秒。
  • path:指定要创建的节点路径。
  • data:可选参数,设置节点的数据内容。
  • acl:可选参数,设置节点的 ACL(Access Control List)权限控制列表
[zk: 127.0.0.1:2181(CONNECTED) 5] create /node data

WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/
Created /node

[zk: 127.0.0.1:2181(CONNECTED) 6] ls /
[node, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 7] ls /node
[]

获取节点数据命令 

get [-s] [-w] path

  • -s:以大小(字节)的格式显示节点的数据内容。
  • -w:监视(Watch)指定路径节点的变化,当节点数据发生变化时,会触发通知。
[zk: 127.0.0.1:2181(CONNECTED) 10] get /node
data
设置或更新节点数据 

set [-s] [-v version] path data

  • -s:以安静(silent)模式设置节点的数据内容,不会输出任何信息。
  • -v version:通过指定版本号来设置节点的数据内容,确保操作的原子性。
[zk: 127.0.0.1:2181(CONNECTED) 11] set /node newdata
[zk: 127.0.0.1:2181(CONNECTED) 12] get /node
newdata
[zk: 127.0.0.1:2181(CONNECTED) 13]
删除节点  

delete [-v version] path

  • -v version:通过指定版本号来删除节点,确保操作的原子性。
[zk: 127.0.0.1:2181(CONNECTED) 13] delete /node
[zk: 127.0.0.1:2181(CONNECTED) 14] ls /
[zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 15]

更多客户端操作请查看官网ZooKeeper: Because Coordinating Distributed Systems is a Zoo

集群模式

安装

集群模式,至少需要三台服务器,强烈建议您使用奇数台服务器。如果您只有两个服务器,那么如果其中一个服务器出现故障,则没有足够的计算机来形成多数仲裁。两台服务器固有地不如一台服务器稳定,因为存在两个单点故障。

集群模式所需的conf/zoo.cfg文件与独立模式中使用的文件相似,但有一些不同。以下是一个示例:

tickTime=2000
dataDir=/home/zookeeper/zookeeper/data
clientPort=2181
initLimit=5
syncLimit=2
server.1=hadoop101:2888:3888
server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888

initLimit,zookeeper用来限制zookeeper服务器连接到leader的时长。

syncLimit,一个服务器多久在leader那里过期。

以上两种过期时间,单位都是tickTime,

本例initLimit时长为5个tickTime=5*2000ms=10秒

server.x列出了所有的zookeeper服务。集群启动它通过查看data下面的myid来知道自己是哪台服务器。

2888为组成zookeeper服务器之间的通信端口,3888为用来选举leader的端口

把同样配置的zookeeper放到hadoop101、hadoop102、hadoop103机器上

在缓存目录(/home/zookeeper/zookeeper/data)下创建对应的myid文件,并写入值

这边以hadoop101为例

[root@hadoop101 data]# echo 1 >> myid
[root@hadoop101 data]# ll
total 8
-rw-r--r--. 1 root root  2 Mar 26 02:13 myid
drwxr-xr-x. 2 root root 37 Mar 26 01:11 version-2
-rw-r--r--. 1 root root  4 Mar 26 01:07 zookeeper_server.pid
[root@hadoop101 data]# cat myid
1
[root@hadoop101 data]#

启动集群

在hadoop101,hadoop102,hadoop103下执行启动命令

[root@hadoop101 bin]# sh zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /home/zookeeper/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@hadoop101 bin]#



[root@hadoop102 bin]# sh zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /home/zookeeper/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED


[root@hadoop103 bin]# sh zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /home/zookeeper/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

分别在hadoop101,hadoop102,hadoop103下执行sh zkServer.sh status

 

 出现上面结果证明集群搭建成功

对集群操作

集群中数据是共享的,只需要在一个服务器中操作,别的服务器会同步数据

sh zkCli.sh -server hadoop101:2181

WatchedEvent state:SyncConnected type:None path:null
[zk: hadoop101:2181(CONNECTED) 0] create /test data
Created /test
[zk: hadoop101:2181(CONNECTED) 1] set /test newdata
[zk: hadoop101:2181(CONNECTED) 2]
sh zkCli.sh -server hadoop102:2181

WatchedEvent state:SyncConnected type:None path:null
[zk: hadoop102:2181(CONNECTED) 0] get /test
data
[zk: hadoop102:2181(CONNECTED) 1] get /test
newdata
[zk: hadoop102:2181(CONNECTED) 2]
sh zkCli.sh -server hadoop103:2181

WatchedEvent state:SyncConnected type:None path:null
[zk: hadoop103:2181(CONNECTED) 0] get /test
data
[zk: hadoop103:2181(CONNECTED) 1] get /test
newdata
[zk: hadoop103:2181(CONNECTED) 2]

整合springboot

依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.18</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.7.18</version>
        </dependency>
    </dependencies>

主启动类和配置

package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author cyz
 * @since 2024/4/8 上午10:23
 */
@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class);
    }
}
zookeeper:
  address: 192.168.31.131:2181
  timeout: 4000

使用场景

分布式锁

实现锁的方式有很多中,这里我们主要介绍两种:悲观锁、乐观锁。

悲观锁

悲观锁 悲观锁认为进程对临界区的竞争总是会出现,为了保证进程在操作数据时,该条数据不被其他进程修改。数据会一直处于被锁定的状态。

我们假设一个具有 n 个进程的应用,同时访问临界区资源,我们通过进程创建 ZooKeeper 节点 /locks 的方式获取锁。

线程 a 通过成功创建 ZooKeeper 节点“/locks”的方式获取锁后继续执行,如下图所示:

这时进程 b 也要访问临界区资源,于是进程 b 也尝试创建“/locks”节点来获取锁,因为之前进程 a 已经创建该节点,所以进程 b 创建节点失败无法获得锁。

这样就实现了一个简单的悲观锁,不过这也有一个隐含的问题,就是当进程 a 因为异常中断导致 /locks 节点始终存在,其他线程因为无法再次创建节点而无法获取锁,这就产生了一个死锁问题。针对这种情况我们可以通过将节点设置为临时节点的方式避免。并通过在服务器端添加监听事件来通知其他进程重新获取锁。

代码如下

import jdk.nashorn.internal.runtime.logging.Logger;
import org.apache.zookeeper.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * @author cyz
 * @since 2024/4/8 上午10:30
 */
@SpringBootTest(classes = Math.class)
public class ZkTest {
    @Value("${zookeeper.address}")
    private    String connectString;
    @Value("${zookeeper.timeout}")
    private  int timeout;
    private final String lockName="/test/lock";
    public ZooKeeper init(){
        ZooKeeper zooKeeper=null;
        try {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            zooKeeper = new ZooKeeper(connectString, timeout, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if(Event.KeeperState.SyncConnected==event.getState()){
                        countDownLatch.countDown();
                    }
                }
            });
            countDownLatch.await();
            System.out.println("【初始化ZooKeeper连接状态....】="+zooKeeper.getState());
        }catch (Exception e){
            System.out.println("初始化ZooKeeper连接异常....】="+e);
        }
        return  zooKeeper;
    }
    @Test
    void testLock() throws InterruptedException, KeeperException {
        ZooKeeper zooKeeper = init();
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(()->{
            try {
                testPessimisticLock1(zooKeeper);
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (KeeperException e) {
                throw new RuntimeException(e);
            }
        }).start();
        testPessimisticLock2(zooKeeper);
        countDownLatch.await();
    }
    void testPessimisticLock1(ZooKeeper zooKeeper) throws InterruptedException, KeeperException {
        while (true){
            try {
                System.out.println(" testPessimisticLock1 开始获取锁");
                String res = zooKeeper.create(lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
                System.out.println(" testPessimisticLock1 获取锁成功=>>>>>"+res);
                break;
            } catch (Exception e) {
                System.out.println(" testPessimisticLock1 获取锁失败");
                TimeUnit.SECONDS.sleep(2);
            }
        }
        TimeUnit.SECONDS.sleep(10);
        System.out.println(" testPessimisticLock1 开始释放锁");
        zooKeeper.delete(lockName,0);
        System.out.println(" testPessimisticLock1 释放锁成功");
    }
    void testPessimisticLock2(ZooKeeper zooKeeper)throws InterruptedException, KeeperException{
        while (true){
            try {
                System.out.println(" testPessimisticLock2 开始获取锁");
                String res = zooKeeper.create(lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
                System.out.println(" testPessimisticLock2 获取锁成功=>>>>>"+res);
                break;
            } catch (Exception e) {
                System.out.println(" testPessimisticLock2 获取锁失败");
                TimeUnit.SECONDS.sleep(2);
            }
        }
        TimeUnit.SECONDS.sleep(10);
        System.out.println(" testPessimisticLock2 开始释放锁");
        zooKeeper.delete(lockName,0);
        System.out.println(" testPessimisticLock2 释放锁成功");
    }

    void optimisticLock1(){

    }
}

乐观锁

乐观锁 乐观锁认为,进程对临界区资源的竞争不会总是出现,所以相对悲观锁而言。加锁方式没有那么激烈,不会全程的锁定资源,而是在数据进行提交更新的时候,对数据的冲突与否进行检测,如果发现冲突了,则拒绝操作。

乐观锁基本可以分为读取、校验、写入三个步骤。CAS(Compare-And-Swap),即比较并替换,就是一个乐观锁的实现。CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。

在 ZooKeeper 中的 version 属性就是用来实现乐观锁机制中的“校验”的,ZooKeeper 每个节点都有数据版本的概念,在调用更新操作的时候,假如有一个客户端试图进行更新操作,它会携带上次获取到的 version 值进行更新。而如果在这段时间内,ZooKeeper 服务器上该节点的数值恰好已经被其他客户端更新了,那么其数据版本一定也会发生变化,因此肯定与客户端携带的 version 无法匹配,便无法成功更新,因此可以有效地避免一些分布式更新的并发问题。

在 ZooKeeper 的底层实现中,当服务端处理 setDataRequest 请求时,首先会调用 checkAndIncVersion 方法进行数据版本校验。ZooKeeper 会从 setDataRequest 请求中获取当前请求的版本 version,同时通过 getRecordForPath 方法获取服务器数据记录 nodeRecord, 从中得到当前服务器上的版本信息 currentversion。如果 version 为 -1,表示该请求操作不使用乐观锁,可以忽略版本对比;如果 version 不是 -1,那么就对比 version 和 currentversion,如果相等,则进行更新操作,否则就会抛出 BadVersionException 异常中断操作。

代码如下

import jdk.nashorn.internal.runtime.logging.Logger;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * @author cyz
 * @since 2024/4/8 上午10:30
 */
@SpringBootTest(classes = Math.class)
public class ZkTest {
    @Value("${zookeeper.address}")
    private    String connectString;
    @Value("${zookeeper.timeout}")
    private  int timeout;
    private final String lockName="/test/lock";
    public ZooKeeper init(){
        ZooKeeper zooKeeper=null;
        try {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            zooKeeper = new ZooKeeper(connectString, timeout, event -> {
                if(Watcher.Event.KeeperState.SyncConnected==event.getState()){
                    countDownLatch.countDown();
                }
            });
            countDownLatch.await();
            System.out.println("【初始化ZooKeeper连接状态....】="+zooKeeper.getState());
        }catch (Exception e){
            System.out.println("初始化ZooKeeper连接异常....】="+e);
        }
        return  zooKeeper;
    }

    @Test
    void testOptimisticLock() throws InterruptedException {
        ZooKeeper zooKeeper=init();
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(()->{
            try {
                optimisticLock1(zooKeeper);
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        optimisticLock2(zooKeeper);
        countDownLatch.await();
    }

    void optimisticLock1(ZooKeeper zooKeeper) throws InterruptedException {
        while (true){
            try {
                Stat stat = new Stat();
                byte[] data = zooKeeper.getData("/test", false, stat);
                TimeUnit.SECONDS.sleep(3);
                System.out.println("optimisticLock1 开始更新数据");
                zooKeeper.setData("/test", "111".getBytes(StandardCharsets.UTF_8), stat.getVersion());
                System.out.println("optimisticLock1 结束更新数据");
                break;
            } catch (KeeperException e) {
                System.out.println("optimisticLock1 更新失败,重新获取锁更新");
                TimeUnit.SECONDS.sleep(1);
            }
        }
    }
    void optimisticLock2(ZooKeeper zooKeeper) throws InterruptedException {
        while (true){
            try {
                Stat stat = new Stat();
                byte[] data = zooKeeper.getData("/test", false, stat);
                TimeUnit.SECONDS.sleep(3);
                System.out.println("optimisticLock2 开始更新数据");
                zooKeeper.setData("/test", "111".getBytes(StandardCharsets.UTF_8), stat.getVersion());
                System.out.println("optimisticLock2 结束更新数据");
                break;
            } catch (KeeperException e) {
                System.out.println("optimisticLock2 更新失败,重新获取锁更新");
                TimeUnit.SECONDS.sleep(1);
            }
        }
    }
}

发布订阅模式

我们可以使用watch事件实现发布订阅功能,要注意一点是,我们提到 Watch 具有一次性,所以当我们获得服务器通知后要再次添加 Watch 事件。

代码如下

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * @author cyz
 * @since 2024/4/8 上午10:30
 */
@SpringBootTest(classes = Math.class)
public class ZkTestTopic {
    @Value("${zookeeper.address}")
    private String connectString;
    @Value("${zookeeper.timeout}")
    private int timeout;
    private final String topic = "/testtopic";

    public ZooKeeper init() {
        ZooKeeper zooKeeper = null;
        try {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            zooKeeper = new ZooKeeper(connectString, timeout, event -> {
                if (Watcher.Event.KeeperState.SyncConnected == event.getState()) {
                    countDownLatch.countDown();
                }
            });
            countDownLatch.await();
            System.out.println("【初始化ZooKeeper连接状态....】=" + zooKeeper.getState());
        } catch (Exception e) {
            System.out.println("初始化ZooKeeper连接异常....】=" + e);
        }
        return zooKeeper;
    }

    @Test
    void testTopic() throws InterruptedException {
        ZooKeeper zooKeeper = init();
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(() -> {
            testProduce(zooKeeper);
            countDownLatch.countDown();
        }).start();
        testConsumer1(zooKeeper, true);
        testConsumer2(zooKeeper, true);
        countDownLatch.await();
    }

    void testConsumer1(ZooKeeper zooKeeper, boolean isInit) {
        Stat stat = new Stat();
        byte[] data = null;
        try {
            data = zooKeeper.getData(topic, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    System.out.println("testConsumer1 消费者消费===>>>>>>>>" + watchedEvent.toString());
                    testConsumer1(zooKeeper, false);
                }
            }, stat);
            if (isInit) {
                System.out.println("testConsumer1 订阅成功");
            }
            System.out.println("testConsumer1 消费消息===>" + new String(data));
        } catch (Exception e) {
            if (isInit) {
                System.out.println("testConsumer1 订阅失败");
            }
        }
    }

    void testConsumer2(ZooKeeper zooKeeper, boolean isInit) {
        Stat stat = new Stat();
        byte[] data = null;
        try {
            data = zooKeeper.getData(topic, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    System.out.println("testConsumer2 消费者消费===>>>>>>>>" + watchedEvent.toString());
                    testConsumer2(zooKeeper, false);
                }
            }, stat);
            if (isInit) {
                System.out.println("testConsumer2 订阅成功");
            }
            System.out.println("testConsumer2 消费消息===>" + new String(data));
        } catch (Exception e) {
            if (isInit) {
                System.out.println("testConsumer2 订阅失败");
            }
        }
    }

    void testProduce(ZooKeeper zooKeeper) {
        int index = 0;
        while (index < 5) {
            try {
                TimeUnit.SECONDS.sleep(1);
                Stat stat = new Stat();
                zooKeeper.getData(topic, false, stat);
                zooKeeper.setData(topic, ("message" + index).getBytes(StandardCharsets.UTF_8), stat.getVersion());
                System.out.println("发布消息成功==>>" + "message" + index);
            } catch (Exception e) {
                System.out.println("发布消息失败==>" + "message" + index);
            }
            index++;
        }
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值