Zookeeper(超级无敌认真好用,万字收藏篇!!!!)

Zookeeper入门

1 Zookeeper概念

  • Zookeeper是apache Hadoop项目下的一个子项目,是一个树形目录服务

  • Zookeeper 翻译过来就是动物园管理员,他是用来管Hadoop(大象)、Hive(蜜蜂)、Pig(小猪)的管理员。简称zk

  • Zookeeper是一个分布式的,开源的分布式应用的协调服务

  • Zookeeper主要功能包括:

    • 配置事务

    • 分布式锁

    • 集群管理

      它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户

2 Zookeeper命令

2.1 Zookeeper数据模型

ZooKeeper是一个树形目录服务,其数据模型和Unix的文件系统目录树很类似,拥有一个层次化结构。

  • 这里面的每一个节点都被称为:ZNode,每个节点上都会保存自己的数据和节点信息。
  • 节点可以拥有子节点,同时也允许少量(1MB)数据存储在该节点之下。
    • PERSISTENT:持久化节点
    • EPHEMERA:临时节点:-e
    • PERSISTENT_SEQUENTIAL持久化顺序节点:-s
    • EPHEMERAL_SEQUENTIAL临时顺序节点:-es
/
/app1
/app2
/app1/p1
/app1/p2
/app1/p3

2.2 Zookeeper服务端常用命令

  • Windows下启动zookeeper
zkServer
  • Linux下常用命令

    启动 ZooKeeper 服务:

      ./zkServer.sh start
    

    查看 ZooKeeper 服务状态:

    ./zkServer.sh status
    

    停止 ZooKeeper 服务 :

     ./zkServer.sh stop
    

    重启 ZooKeeper 服务 :

      ./zkServer.sh restart
    

2.3 Zookeeper客户端常用命令

  • 打开另外一个cmd运行客户端

    zkCli
    
  • 常用客户端命令如下:

    • 获取帮助

         help
      
    • 客户端断开连接

      quit
      
    • 查看子节点

      //ls / 表示查看根节点下的子节点
      ls 父节点路径(父节点全路径或相对路径)
      
    • 创建子节点(默认是持久化节点)

          /*
          比如 create /jobs 表示在 / 下创建 jobs 子节点
        	可以给节点存储数据,也可以不存储数据
          比如 create /jobs date 表示在 / 下创建 jobs 子节点并存储的数据为 date  
        	*/
          create 父节点路径 [数据内容] 
      
    • 获取节点的数据

          // 比如 get /jobs 表示获取 jobs 节点中存储的数据,应该会获取到 date
          get 节点全路径或相对路径
      
    • 设置或修改节点的数据值

      //比如 set /jobs newdate 表示将 jobs 节点的数据设置为 newdate
      			set 节点全路径或相对路径 [数据内容]
      
    • 删除单个节点

      /*
      			比如 delete /jobs 表示删除 / 下面的 jobs 节点
      			  但是如果 jobs 节点下面有子节点的话,该命令就无法删除 jobs 节点了
      			*/
      			delete 节点全路径或相对路径
      
      • 删除带有子节点的节点
      //比如 deleteall /test 表示删除 / 下面的 test 以及 test 下的所有节点
      deleteall 节点全路径或相对路径
      

2.4 客户端命令-创建临时有序节点

  • 创建临时节点,增加 -e 参数
//当客户端断开与 zookeeper 的拦截后,所创建的临时节点会自动被删除掉
create -e 子节点全路径或相对路径 [数据内容]
  • 创建顺序节点,增加 -s 参数
/*
创建的节点名称,会自动被添加上数字编号
	比如多次运行 create -s /aaa 后,通过 ls / 查看 / 下面的子节点列表
	会发现类似 /aaa0000000001 /aaa0000000002 /aaa0000000003 的顺序节点
*/
create -s 子节点全路径或相对路径 [数据内容]
  • 创建临时顺序节点,增加 -es参数
/*
临时顺序节点,兼有临时节点和顺序节点的特性,常用于分布式所的应用场景
*/
create -es 子节点全路径或相对路径 [数据内容]
  • 查看一个节点的详细信息
ls -s 节点全路径或相对路径
  • czxid:节点被创建的事务ID

  • ctime:节点的创建时间

  • mtime: 修改时间

  • pzxid:子节点列表最后一次被更新的事务ID

  • cversion:子节点的版本号

  • dataversion:数据版本号

  • aclversion:权限版本号

  • ephemeralOwner:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0

  • dataLength:节点存储的数据的长度

  • numChildren:当前节点的子节点个数

3 JavaAPI操作Zookeeper

3.1 Curator的介绍

Curator是 ApacheZooKeeper 的Java客户端库。

  • 常见ZookeeperJavaAPI:
    • 原生JavaAPI
    • zkClient
    • Curator
  • Curator项目的目标是简化ZooKeeper客户端的使用。
  • Curator最初是Netflix研发的,后来捐献了Apache基金会,目前是Apache的顶级项目。

3.2 建立连接

  • 导入依赖

    此处会有curator和zookeeper版本兼容问题,需注意

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.3.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.3.0</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.36</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.36</version>
        </dependency>
    </dependencies>
  • 日志配置
### 设置日志级别###
#log4j.rootLogger = debug,stdout,D,E

log4j.rootLogger = off,stdout
### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n

### 输出DEBUG 级别以上的日志到=E://logs/error.log ###
#log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
#log4j.appender.D.File = E://logs/log.log
#log4j.appender.D.Append = true
#log4j.appender.D.Threshold = DEBUG
#log4j.appender.D.layout = org.apache.log4j.PatternLayout
#log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n
#
### 输出ERROR 级别以上的日志到=E://logs/error.log ###
#log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
#log4j.appender.E.File =E://logs/error.log
#log4j.appender.E.Append = true
#log4j.appender.E.Threshold = ERROR
#log4j.appender.E.layout = org.apache.log4j.PatternLayout
#log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n
  • 建立连接

    建立连接四个参数
connectString 连接字符串:zk server地址和端口 127.0.0.1:2181,127.0.0.1:2182
sessionTimeoutMs 会话超时时间: 单位ms,默认60 * 1000
connectionTimeoutMs 连接超时时间: 单位ms ,默认15 * 1000
retryPolicy 重试策略
  • 第一种方式
  /*
    重试策略
    参数1:间隔多长时间重试
    参数2:重试多少次
  */
  RetryPolicy retryPolicy =new ExponentialBackoffRetry(3000,10);
  /**
   * 第一种方式
   * 参数1 connectString:       连接字符串:zk server地址和端口 127.0.0.1:2181,127.0.0.1:2182
   * 参数2 sessionTimeoutMs:    会话超时时间: 单位ms
   * 参数3 connectionTimeoutMs: 连接超时时间: 单位ms
   * 参数4 retryPolicy:         重试策略
   */
  CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181",
                                                              60 * 1000, 
                                                              15 * 1000, 
                                                              retryPolicy);
  
  client.start();
  • 第二种方式
  /*
    重试策略
    参数1:间隔多长时间重试
    参数2:重试多少次
  */
  RetryPolicy retryPolicy =new ExponentialBackoffRetry(3000,10);
  //第二种方式
  CuratorFramework client = CuratorFrameworkFactory.builder()
          .connectString("127.0.0.1:2181")
          .sessionTimeoutMs(60 * 1000)
          .connectionTimeoutMs(15 * 1000)
          .retryPolicy(retryPolicy)
          .namespace("hello") //设置操作根目录
          .build();
  client.start();

3.3 创建节点

创建节点:create 临时 持久 顺序 数据

  1. 基本创建
  2. 设置节点,带有数据
  3. 设置节点类型
  4. 创建多级节点
  1. 基本创建

    创建节点,若节点没有数据,则默认节点数据为ip地址

    语法: client.create().forPath("")

    @Test
    public void testCreat() throws Exception {
          
          
            //基本创建
            //创建节点,若节点没有数据,则默认节点数据为ip地址
            client.create().forPath("/app1");
        }
    
    [zk: localhost:2181(CONNECTED) 2]  ls /
    [dubbo, hello, services, zookeeper]
    [zk: localhost:2181(CONNECTED) 3] ls /hello
    [app1]
    [zk: localhost:2181(CONNECTED) 4] get /hello/app1
    192.168.205.116
    
  2. 设置节点,带有数据

    语法:client.create().forPath("",data;

    @Test
    public void testCreat2() throws Exception {
          
          
        //设置节点,带有数据
        client.create().forPath("/app2","hello zookeeper".getBytes());
    }
    
    [zk: localhost:2181(CONNECTED) 5] get /hello/app2
    hello zookeeper
    
  3. 设置节点类型

    PERSISTENT:持久化节点

    EPHEMERA:临时节点:-e

    PERSISTENT_SEQUENTIAL持久化顺序节点:-s

    EPHEMERAL_SEQUENTIAL临时顺序节点:-es

    语法:client.create().withMode(节点类型).forPath()

    @Test
    public void testCreat3() throws Exception {
          
          
        //设置节点,带有数据
        client.create().withMode(CreateMode.EPHEMERAL ).forPath("/app3","hello zookeeper".getBytes());
        //加个延时试一下当前会话没关闭时,该节点在吗
        TimeUnit.MILLISECONDS.sleep(1000*60);
    }
    
  4. 创建多级节点

    creatingParentsIfNeeded,如果父节点不存在创建父节点

    语法:client.create().creatingParentsIfNeeded().forPath()

    @Test
    public void testCreat4() throws Exception {
          
          
        //设置节点,带有数据
        //creatingParentsIfNeeded,如果父节点不存在创建父节点
        client.create().creatingParentsIfNeeded().forPath("/app4/p1","hello zookeeper".getBytes()); 
    }
    

3.4 查询节点

  1. 查询节点数据 get

    语法:client.getData().forPath("")

    @Test
    public void testGet() throws Exception {
          
          
        byte[] bytes = client.getData().forPath("/app1");
        System.out.println(new String(bytes));
    }
    
    • 控制台输出
    192.168.205.116
    
  2. 查询子节点 ls

    PS:注意此处的"/"是指定的hello节点,并不是zkcli的根节点

    语法:client.getChildren().forPath("")

    @Test
    public void testGet2() throws Exception {
          
           
        List<String> list = client.getChildren().forPath("/");
        System.out.println(list);
    }
    
    • 控制台输出
    [app2, app1, app4]
    
  3. 查询节点状态信息 ls -s

    语法:client.getData().storingStatIn(stat).forPath("/app4")

    stat 为状态对象

    里面包括:

    • czxid:节点被创建的事务ID

    • ctime:节点的创建时间

    • mtime: 修改时间

    • pzxid:子节点列表最后一次被更新的事务ID

    • cversion:子节点的版本号

    • dataversion:数据版本号

    • aclversion:权限版本号

    • ephemeralOwner:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0

    • dataLength:节点存储的数据的长度

    • numChildren:当前节点的子节点个数

    @Test
    public void testGet3() throws Exception {
          
          
        //创建stat
        Stat stat    =new Stat();
        System.out.println(stat);
        System.out.println("------查询之后-----");
        client.getData().storingStatIn(stat).forPath("/app4");
        System.out.println(stat);
    }
    
    • 控制台输出
    0,0,0,0,0,0,0,0,0,0,0
    
    ------查询之后-----
    387,387,1681452139819,1681452139819,0,1,0,0,0,1,388
    

3.5 修改节点

  1. 修改数据

    语法:client.setData().forPath("",Data);

    @Test
    public void testSet() throws Exception {
          
          
      client.setData().forPath("/app1","setapp1date".getBytes());
    }
    
    [zk: localhost:2181(CONNECTED) 11] get /hello/app1
    setapp1date
    
  2. 根据版本修改

    语法client.setData().withVersion(version).forPath("", data);

    version:查询出来的,目的是为了让其他客户端/线程不干扰该次修改操作;

    @Test
    public void testSet2() throws Exception {
          
          
        Stat stat = new Stat();
        client.getData().storingStatIn(stat).forPath("/app1");
        int version =stat.getVersion();//查询当前节点状态的版本
        System.out.println(version);
        client.setData().withVersion(version).forPath("/app1", "hhahaha".getBytes());//修改一次vsrsion++
    }
    

3.6 删除节点

  1. 删除单个节点

    语法:client.delete().forPath("");

    @Test
    public void testDelete() throws Exception {
          
           
       client.delete().forPath("/app1"); 
    }
    
  2. 删除带有子节点的节点

    deletingChildrenIfNeeded:带有子节点的节点

    语法client.delete().deletingChildrenIfNeeded().forPath("");

    @Test
    public void testDelete2() throws Exception {
          
          
        client.delete().deletingChildrenIfNeeded().forPath("/app4");
    }
    
  3. 必须成功的删除

    防止网络抖动,本质就是重试

    guaranteed:保证,必须

    语法:client.delete().guaranteed().forPath("");

    @Test
    public void testDelete3() throws Exception {
          
               		                  client.delete().guaranteed().forPath("/app2");
    }
    
  4. 回调

    绑定一个回调函数,删除成功后自动执行该方法。。。。

    语法:client.delete().inBackground(回调函数).forPath("");

    @Test
    public void testDelete4() throws Exception {
          
          
        client.delete().inBackground(new BackgroundCallback() {
          
          
            @Override
            public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
          
          
                System.out.println("删除成功....");
                System.out.println(curatorEvent);
            }
        }).forPath("/app1"); 
    }
    
    • 控制台打印
    删除成功....
    CuratorEventImpl{type=DELETE, resultCode=0, path='/app1', name='null', children=null, context=null, stat=null, data=null, watchedEvent=null, aclList=null, opResults=null}
    

4 Curator-Watch事件监听

4.1 Watch 概述

  • ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。

  • ZooKeeper中引入了Watcher机制来实现了发布/订阅功能能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。

  • ZooKeeper原生支持通过注册Watcher来进行事件监听,但是其使用并不是特别方便需要开发人员自己反复注册Watcher,比较繁琐。

  • Curator引入了Cache来实现对ZooKeeper服务端事件的监听。

    • Cache不止可以用来事件监听,还可以来实现Zookeeper节点数据的缓存
  • ZooKeeper提供了三种Watcher:

    • NodeCache 只是监听某一个特定的节点
    • PathChildrenCache:监控一个ZNode的子节点.
    • TreeCache:可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合
  • CuratorCache:可以监控整个树上的所有节点


在curator 5.1.0后, NodeCachePathChildrenCacheTreeCache,被弃用。使用新的CuratorCache进行监听

  • 由于学习原因,以及往后公司可能会用低版本的curator,所有此处旧的Watcher和新Watcher的都会记录

4.2 NodeCatch

NodeCache 只是监听某一个特定的节点

  • 给单一节点注册监听器
    @Test
    public void testNodeCatch() throws Exception {
    
    
        //1. 创建NodeCatch对象
        NodeCache nodeCache =new NodeCache(client,"/app1");
        //2. 注册监听
        nodeCache.getListenable().addListener(new NodeCacheListener() {
    
    
            @Override
            public void nodeChanged() throws Exception {
    
    
                System.out.println("节点改变了...");
                //获取修改后当前节点的数据
                byte[] data = nodeCache.getCurrentData().getData();
                System.out.println(new String(data));
            }
        });
        //3. 开启监听,若果设置为true,开启监听
        nodeCache.start(true);
        //此处延时只是让,会话不会快速关闭,以便测试监听功能
        while (true){
    
    
        }
    }

4.3 PathChildrenCache

监控一节点的的子节点

    @Test
    public void testPathChildrenCache() throws Exception {
    
    
        //1. 创建NodeCatch对象
        PathChildrenCache childrenCache =new PathChildrenCache(client,"/app2",true);
        //2. 注册监听
        childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
    
    
            //类似于删除节点的回调
            @Override
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
    
    
                System.out.println("子节点变化了....");
                System.out.println(pathChildrenCacheEvent);
                //监听变更,拿到变更后的数据
                // 1.获取类型
                PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
                //2. 判断类型是否是update
                if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
    
    
                    System.out.println("数据改变");
                    byte[] data = pathChildrenCacheEvent.getData().getData();
                    System.out.println(new String(data));
                }
            }
        });
        //3. 开启监听,若果设置为true,开启监听
        childrenCache.start(true);
        //此处延时只是让,会话不会快速关闭,以便测试监听功能
        while (true){
    
    
        }
    }

4.4 TreeCache

可以监控整个树上的所有节点

    @Test
    public void testTreeCache() throws Exception {
    
    
        //1. 创建NodeCatch对象
        TreeCache treeCache =new TreeCache(client,"/app2");
        //2. 注册监听
        treeCache.getListenable().addListener(new TreeCacheListener() {
    
    
            @Override
            public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
    
    
                System.out.println("节点变化");
                System.out.println(treeCacheEvent);
            }
        });
        //3. 开启监听,若果设置为true,开启监听
        treeCache.start();
        //此处延时只是让,会话不会快速关闭,以便测试监听功能
        while (true){
    
    
        }
    }

4.5 CuratorCache

可以监控整个树上的所有节点

    @Test
    public void testCuratorCache() throws Exception {
    
    
        //1. 创建NodeCatch对象
        CuratorCache treeCache =CuratorCache.build(client,"/app2");
        //2. 注册监听
        treeCache.listenable().addListener(new CuratorCacheListener() {
    
    
            @Override
            public void event(Type type, ChildData oldData, ChildData data) {
    
    
                if (type.equals(Type.NODE_CHANGED)){
    
    //节点发生改变
                    String path = oldData.getPath();
                    System.out.println("节点"+path+"改变");
                    System.out.println("----------老数据-----------");
                    byte[] oldDataStr = oldData.getData();
                    System.out.println(new String(oldDataStr));
                    System.out.println("----------新数据-----------");
                    byte[] dataStr = data.getData();
                    System.out.println(new String(dataStr));
                }
            }
        });
        //3. 开启监听,若果设置为true,开启监听
        treeCache.start();
        //此处延时只是让,会话不会快速关闭,以便测试监听功能
        while (true){
    
    
        }
    }

5 分布式锁

5.1 分布式锁概念

  • 在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题。

  • 但当我们的应用是分布式集群工作的情况下,属于多VM下的工作环境,跨IVM之间已经无法通过多线程的锁解决同步问题。

  • 那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题——这就是分布式锁。

PS:也就是说在多机情况下,多个机器都可以修改同一数据,就算我们给每个机器都加锁了,但是会出现多个机器同时修改该数据,会发生数据错乱。。。

解决方法

① 基于数据库的分布式锁

  • 唯一性约束

    当我们多个服务都要执行,插入或修改操作,数据库唯一性约束保证,只能有一个服务可以成功。。。

  • 基于数据库排他锁

    获得排它锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁connection.commit()。当某条记录被加上排他锁之后,其他线程无法获取排他锁并被阻塞。

缺点:操作数据库开销大,进行优化会过于复杂,性能不高,不建议

② 基于缓存的分布式锁

Redis中没有分布式锁,我们可以使用setnx设计一个分布式锁

setnx:如果key存在存值失败,如果不存在则存值

  • 添加一个key,该key为分布式锁,我们知道setnx在设置数据时如果数据存在则返回0

  • 设置该数据为锁,其他客户端要操作数据前先通过该指令的返回值检测如果返回值为0
    则表示当前数据已被锁定不能操作,如果返回值为1表示加锁,然后操作。

    setnx lock-num 1

  • 对加锁的数据使用后要解锁,通过del lock-num移除数据的方式实现解锁过程
    del lock-num

③ 基于Zookeeper的分布式锁

5.2 Zookeeper分布式锁原理

核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点。

  1. 客户端获取锁时,在lock节点下创建临时顺字节点。
  2. 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
  3. 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
  4. 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。

PS:在lock节点下,创建临时顺序节点,序号小的先获取锁,若不是序号最小的,给比他还小的添加删除事件监听器,当小的被删除,再次判断是不是最小的,依次…

  • 为啥要是临时节点?

    发生宕机时,该锁也可以被删除

5.3 模拟分布式锁案例-售票服务

无多机条件,只是用多线程模拟

  • 在Curator有五种锁方案:

    • InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
    • InterProcessMutex:分布式可重入排它锁
    • InterProcessReadWriteLock:分布式读写锁
    • InterProcessMultiLock:将多个锁作为单个实体管理的容器
    • lnterProcessSemaphoreV2:共享信号量
  • 售票类

public class Ticket implements Runnable{
    
    
    private Integer tickets =20;//票数
    private CuratorFramework client;
    private InterProcessMutex lock;   //声明可重入排他性锁

    public Ticket() {
    
    
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
        client = CuratorFrameworkFactory.builder()
                .connectString("127.0.0.1:2181")      //链接端口号	
                .sessionTimeoutMs(60 * 1000)		  //会话超时时间
                .connectionTimeoutMs(15 * 1000)		  //连接超时时间		
                .retryPolicy(retryPolicy)			  //重试机制	
                .build();
        client.start();
        lock =new InterProcessMutex(client,"/lock");
    }

    @Override
    public void run() {
    
    
        while (true){
    
    
            try {
    
    
                //获取锁
                lock.acquire(3000, TimeUnit.MILLISECONDS);
                if (tickets>0){
    
    
                    //哪个线程买票
                    System.out.println(Thread.currentThread()+":"+tickets);
                    TimeUnit.MILLISECONDS.sleep(100);
                    tickets--;
                }
                if (tickets==0){
    
    
                    System.out.println("票售空了...");
                    break;
                }
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }finally {
    
    
                try {
    
    
                    //释放锁
                    lock.release();
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 买票类
public class LockTest {
    
    
    public static void main(String[] args) {
    
    
        Ticket ticket =new Ticket();
		//三个线程,一个线程模拟一台机器
        Thread t1 =new Thread(ticket,"p1");
        Thread t2 =new Thread(ticket,"p2");
        Thread t3 =new Thread(ticket,"p3");

        t1.start();
        t2.start();
        t3.start();
    }
}

6 集群

6.1 集群介绍

Leader选举:

  • Serverid:服务器ID

    • 比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大。
  • Zxid:数据ID

    • 服务器中存放的最大数据ID.值越大说明数据越新,在选举算法中数据越新权重越大。
  • 在Leader选举的过程中,如果某台ZooKeeper获得了超过半数的选票,则此ZooKeeper就可以成为Leader了。

Zookeeper的三种角色

在这里插入图片描述

Zookeeper 集群中Server有三种角色,Leader、Follower 和 Observer

  • Leader:负责投投票的发起与决议,更新系统状态,写数据

  • Follower:用于接收客户端请求并用来返回结果,在选主过程中参与投票

  • Observer:可以接受客户端连接,将写请求转发给leader节点,但是不参与投票过程,只同步leader状态,主要存在目的就是为了提高读取效率

    引进Observer角色作用:Zookeeper需保证高可用和强一致性,为了支持更多的客户端,需要增加更多 Server;Server 增多,投票阶段延迟增大,影响性能;引入 Observer, Observer不参与投票; Observer接受客户端的连接,并将写请求转发给leader节点; 加入更多Observer节点,提高伸缩性,同时不影响吞吐率。

Zookeeper 建议集群节点个数为奇数,只要超过一半的机器能够正常提供服务,那么整个集群都是可用的状态,最少满足2n+1台(n >= 1)


Zookeeper 的数据一致性是依靠ZAB协议完成的。

ZAB(ZooKeeper Atomic Broadcast 原子广播)协议

  • 是为 ZooKeeper 特殊设计的一种支持崩溃恢复的原子广播协议。在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式(即Leader和Follower模型)的系统架构来保持集群中各个副本之间的数据一致性。

  • ZAB 协议包括有两种模式,分别是 崩溃恢复和消息广播。

  • 当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务 器开始与新的 Leader 服务器进行数据同步,当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行处理。


在这里插入图片描述

1.在Client向Follwer发出一个写的请求
2.Follwer把请求发送给Leader
3.Leader接收到以后开始发起投票并通知Follwer进行投票
4.Follwer把投票结果发送给Leader
5.Leader将结果汇总后如果需要写入,则开始写入同时把写入操作通知给Leader,然后commit;
6.Follwer把请求结果返回给Client

Follower主要有四个功能:
  1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
  2. 接收Leader消息并进行处理;  
  3. 接收Client的请求,如果为写请求,发送给Leader进行投票;
  4. 返回Client结果。

6.2 集群搭建

真实的集群是需要部署在不同的服务器上的,但是在我们测试时同时启动很多个虚拟机内存会吃不消,所以我们通常会搭建伪集群,也就是把所有的服务都搭建在一台虚拟机上,用端口进行区分。

我们这里要求搭建一个三个节点的Zookeeper集群(伪集群)。

本人电脑配置有限,不能同时启动三个Zookeeper…

猜你喜欢

转载自blog.csdn.net/woschengxuyuan/article/details/130163029
今日推荐