zookeeper的使用
1、数据模型
zk的存储结构和标准的文件系统非常类似,每一个节点称之为ZNode,是zk的最小单元。每个ZNode上都可以保存数据以及添加子节点,形成一个层次化的树形结构。节点类型有以下几种:
(1)持久节点(PERSISTENT),创建后会一直在zk服务器上,直到主动删除。
(2)持久有序节点(PERSISTENT_SEQUENTIAL),同一级节点保持有序性。
(3)临时节点(EPHEMERAL),临时节点的生命周期和客户端的会话绑定在一起,当客户端会话 失效该节点自动清理 。
(4)临时有序节点
zk通过版本控制来保证分布式数据原子性:zk中每个数据节点有三个版本信息,对数据节点的任何更新操作都会引起版本号的变化,其原理和乐观锁类似。
version:当前数据节点内容的版本号
cversion:当前数据节点子节点的版本号
aversion:当前数据节点ACL(访问权限)变更版本号
2、zookeeper常用java客户端
zk比较常用的java客户端是curator,它封装了zk client和server之间的连接处理,并且封装了很多应用场景(分布式锁、leader选举)。
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.0.0</version> </dependency> //建立连接 CuratorFramework curatorFramework=CuratorFrameworkFactory. builder (). connectString( CONNECTION_STR ).sessionTimeoutMs(5000) . retryPolicy(new ExponentialBackoffRetry(1000,3)).namespace(“curator ”).build();
重试策略:Curator内部实现的几种重试策略:
• ExponentialBackoffRetry:重试指定的次数, 且每一次重试之 间停顿的时间逐渐增加.
• RetryNTimes:指定最大重试次数的重试策略
• RetryOneTime:仅重试一次
• RetryUntilElapsed:一直重试直到达到规定的时间
namespace: session会话含有隔离命名空间,即客户端对 Zookeeper 上数据节点的任何操作都是相对/curator 目录进行的,这有利于实现不同的 Zookeeper 的业务之间的隔离。
zk client与zk server连接状态:
连接状态 | 状态含义 |
---|---|
KeeperState.Expired | 客户端和服务器在ticktime的时间周期内,是要发送心跳通知的。这是租约协议的一个实现。客户端发送request,告诉服务器其上一个租约时间,服务器收到这个请求后,告诉客户端其下一个租约时间是哪个时间点。当客户端时间戳达到最后一个租约时间,而没有收到服务器发来的任何新租约时间,即认为自己下线(此后客户端会废弃这次连接,并试图重新建立连接)。这个过期状态就是Expired状态 |
KeeperState.Disconnected | 就像上面那个状态所述,当客户端断开一个连接(可能是租约期满,也可能是客户端主动断开)这是客户端和服务器的连接就是Disconnected状态 |
KeeperState.SyncConnected | 一旦客户端和服务器的某一个节点建立连接(注意,虽然集群有多个节点,但是客户端一次连接到一个节点就行了),并完成一次version、zxid的同步,这时的客户端和服务器的连接状态就是SyncConnected |
KeeperState.AuthFailed | zookeeper客户端进行连接认证失败时,发生该状态 |
3、zk事件监听机制(Watcher)
Watcher监听机制是Zookeeper中非常重要的特性,可以绑定监听事件的节点进行监听,比如可以监听节点数据变更、节点删除、子节点状态变更等 事件,通过这个事件机制,可以基于zookeeper实现分布式 锁、集群管理等功能。
节点事件 | 事件含义 |
EventType.NodeCreated | 节点被创建时,该事件被触发 |
EventType.NodeChildrenChanged | 节点的直接子节点被创建、被删除、子节点数据发生变更时,该事件被触发 |
EventType.NodeDataChanged | 节点的数据发生变更时,该事件被触发 |
EventType.NodeDeleted | 节点被删除时,该事件被触发 |
EventType.None | 当zookeeper客户端的连接状态发生变更时,该事件被触发 |
watcher 机制有一个特性:当数据发生改变的时候,那么 zookeeper会产生一个watch事件并发送到客户端,但是客户端只会收到一次这样的通知,如果以后这个数据再发生变 化,那么之前设置 watch的客户端不会再次收到消息。因为它是一次性的;如果要实现永久监听,可以通过循环注册来实现。
Curator提供了三种Watcher来监听节点的变化:
• PathChildCache:监视一个路径下孩子结点的创建、删 除、更新。
• NodeCache:监视当前结点的创建、更新、删除,并将 结点的数据缓存在本地。
• TreeCache:PathChildCache和NodeCache的“合体”, 监视路径下的创建、更新、删除事件,并缓存路径下所 有孩子结点的数据。
4、基于Curator实现分布式锁
4.1、zookeeper实现分布式锁的原理
第一种:利用单节点实现分布式锁
利用zookeeper同级节点的唯一性来实现排他锁。多个线程向zk指定的节点下创建一个相同名称的节点,只有一个能成功(成功的则获取锁),其他失败的线程通过watcher机制来监听zk这个子节点的变化,一旦监听到这个子节点的删除事件(删除则释放锁),则再次触发所有节点去竞争锁(即创建子节点)。
这种实现方式很简单,但是会产生“惊群效应”,简单来说就是如果存在许多的客户端在等待获取锁,当成功获取到锁的进程释放该节点后,所有处于等待状态的客户端都会被唤醒,这个时候zookeeper在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会 对zookeeper服务器的性能产生比较的影响。
第二种:利用临时有序节点实现分布式锁
每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么可以认为子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。这种实现和前面单节点实现的差异性在于,每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到watcher事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致惊群效应,因为每个客户端只需要监控一个节点。
4.2、curator分布式锁的基本使用
curator对于锁这块做了一些封装,提供了 InterProcessMutex 这样一个api。除了分布式锁之外,还提供了leader选举、分布式队列等常用的功能。
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
public class Demo { public static void main(String[] args) { CuratorFramework curatorFramework=null; curatorFramework=CuratorFrameworkF actory.builder(). connectString(ZkConfig.ZK_CONNECT_ STR). sessionTimeoutMs(ZkConfig.ZK_SESSI ON_TIMEOUT). retryPolicy(new ExponentialBackoffRetry(1000,10)). build(); curatorFramework.start(); final InterProcessMutex lock=new InterProcessMutex(curatorFramework ,"/locks"); for(int i=0;i<10;i++){ new Thread(()->{ System.out.println(Thread.currentT hread().getName()+"->尝试获取锁"); try { lock.acquire(); System.out.println(Thread.currentT hread().getName()+"->获得锁成功"); } catch (Exception e) { e.printStackTrace(); } try { Thread.sleep(4000); lock.release(); System.out.println(Thread.currentT hread().getName()+"->释放锁成功"); } catch (Exception e) { e.printStackTrace(); } },"t"+i).start();
} } }
5、curator实现leader选举
curator有两种选举策略:
第一种:Leader Latch——与分布式锁的实现一样
参与选举的所有节点,会创建一个顺序节点,其中最小的节点会设置为master节点, 没抢到Leader的节点都监听前一个节点的删除事件,在前一个节点删除后进行重新抢 主,当master节点手动调用close()方法或者master 节点挂了之后,后续的子节点会抢占master。
第二种:LeaderSelector
LeaderSelector和Leader Latch最的差别在于,leader 在释放领导权以后,还可以继续参与竞争。
案例演示:
public class SelectorClient2 extends LeaderSelectorListenerAdapter implements Closeable { private final String name; private final LeaderSelector leaderSelector; public SelectorClient2(CuratorFramework client, String path, String name) { this.name = name; // 利用一个给定的路径创建一个 leader selector // 执行 leader 选举的所有参与者对应的路径必须一样 // 本例中 SelectorClient 也是一个LeaderSelectorListener ,但这不是必须的。 leaderSelector = new LeaderSelector(client, path, this); // 在大多数情况下,我们会希望一个 selector放弃 leader 后还要重新参与 leader 选举 leaderSelector.autoRequeue(); } public void start(){ leaderSelector.start(); } @Override public void close() throws IOException { leaderSelector.close(); } @Override public void takeLeadership(CuratorFramework curatorFramework) throws Exception { System. out .println(name + " 现在是 leader了,持续成为 leader "); // 选举为 master , System. in .read();//阻塞,让当前获得 leader权限的节点一直持有,直到该进程关闭 } private static String CONNECTION_STR ="192.168.13.102:2181,192.168.13.103:2181,192.168.13.104:2181"; public static void main(String[] args) throws IOException { CuratorFramework curatorFramework= CuratorFrameworkFactory. builder (). connectString( CONNECTION_STR ).sessionTimeoutMs(5000). retryPolicy(new ExponentialBackoffRetry(1000,3)).build(); curatorFramework.start(); SelectorClient2 sc=new SelectorClient2(curatorFramework,"/leader","ClientB"); sc.start(); System. in .read(); }
EventType.None |
当zookeeper客户端的连接状态发生变更时,该事件被触发 |