1.9 分布式协调服务-Zookeeper(二)
zoo.cfg配置文件分析
tickTime=2000 zookeeper中最小的时间单位长度 (ms)
initLimit=10 follower节点启动后与leader节点完成数据同步的时间
syncLimit=5 leader节点和follower节点进行心跳检测的最大延时时间
dataDir=/tmp/zookeeper 表示zookeeper服务器存储快照文件的目录
dataLogDir 表示配置 zookeeper事务日志的存储路径,默认指定在dataDir目录下
clientPort 表示客户端和服务端建立连接的端口号: 2181
zookeeper中的一些概念
数据模型
zookeeper的数据模型和文件系统类似,每一个节点称为:znode. 是zookeeper中的最小数据单元。每一个znode上都可以
保存数据和挂载子节点。 从而构成一个层次化的属性结构
节点特性
持久化节点 : 节点创建后会一直存在zookeeper服务器上,直到主动删除
持久化有序节点 :每个节点都会为它的一级子节点维护一个顺序
临时节点 : 临时节点的生命周期和客户端的会话保持一致。当客户端会话失效,该节点自动清理
临时有序节点 : 在临时节点上多勒一个顺序性特性
会话
客户端与服务器的一次连接
Watcher
zookeeper提供了分布式数据发布/订阅,zookeeper允许客户端向服务器注册一个watcher监听。当服务器端的节点触发指定事件的时候
会触发watcher。服务端会向客户端发送一个事件通知
watcher的通知是一次性,一旦触发一次通知后,该watcher就失效
ACL
zookeeper提供控制节点访问权限的功能,用于有效的保证zookeeper中数据的安全性。避免误操作而导致系统出现重大事故。
CREATE /READ/WRITE/DELETE/ADMIN
zookeeper的命令操作
1. create [-s] [-e] path data acl
-s 表示节点是否有序
-e 表示是否为临时节点
默认情况下,是持久化节点
2. get path [watch]
获得指定 path的信息
3.set path data [version]
修改节点 path对应的data
乐观锁的概念
数据库里面有一个 version 字段去控制数据行的版本号
4.delete path [version]
删除节点
stat信息
cversion = 0 子节点的版本号
aclVersion = 0 表示acl的版本号,修改节点权限
dataVersion = 1 表示的是当前节点数据的版本号
czxid 节点被创建时的事务ID
mzxid 节点最后一次被更新的事务ID
pzxid 当前节点下的子节点最后一次被修改时的事务ID
ctime = Sat Aug 05 20:48:26 CST 2017
mtime = Sat Aug 05 20:48:50 CST 2017
cZxid = 0x500000015
ctime = Sat Aug 05 20:48:26 CST 2017
mZxid = 0x500000016
mtime = Sat Aug 05 20:48:50 CST 2017
pZxid = 0x500000015
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0 创建临时节点的时候,会有一个sessionId 。 该值存储的就是这个sessionid
dataLength = 3 数据值长度
numChildren = 0 子节点数
java API的使用
1.导入java包
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.8</version> </dependency>
权限控制模式
schema:授权对象
ip : 192.168.1.1
Digest : username:password
world : 开放式的权限控制模式,数据节点的访问权限对所有用户开放。 world:anyone
super :超级用户,可以对zookeeper上的数据节点进行操作
连接状态
KeeperStat.Expired 在一定时间内客户端没有收到服务器的通知, 则认为当前的会话已经过期了。
KeeperStat.Disconnected 断开连接的状态
KeeperStat.SyncConnected 客户端和服务器端在某一个节点上建立连接,并且完成一次version、zxid同步
KeeperStat.authFailed 授权失败
事件类型
NodeCreated 当节点被创建的时候,触发
NodeChildrenChanged 表示子节点被创建、被删除、子节点数据发生变化
NodeDataChanged 节点数据发生变化
NodeDeleted 节点被删除
None 客户端和服务器端连接状态发生变化的时候,事件类型就是None
zookeeper的实际应用场景
zookeeper能够实现哪些场景
订阅发布
watcher机制
统一配置管理(disconf)
分布式锁
1.应用:redis ,zookeeper数据库
2.zookeeper分布式锁实现原理
-
客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推;
-
客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;
-
执行业务代码;
-
完成业务流程后,删除对应的子节点释放锁。
负载均衡
ID生成器
分布式队列
统一命名服务
master选举
封装的zookeeper——curator
curator
Curator本身是Netflix公司开源的zookeeper客户端;
curator提供了各种应用场景的实现封装
curator-framework 提供了fluent风格api
curator-replice 提供了实现封装
curator连接的重试策略
ExponentialBackoffRetry() 衰减重试
RetryNTimes 指定最大重试次数
RetryOneTime 仅重试一次
RetryUnitilElapsed 一直重试知道规定的时间
curator使用分布式锁
虽然zookeeper原生客户端暴露的API已经非常简洁了,但是实现一个分布式锁还是比较麻烦的。
1.引入包:
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.0.0</version> </dependency>
2.使用分布式锁代码
public static void main(String[] args) throws Exception { //创建zookeeper的客户端 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.newClient("10.21.41.181:2181,10.21.42.47:2181,10.21.49.252:2181", retryPolicy); client.start(); //创建分布式锁, 锁空间的根节点路径为/curator/lock InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock");
//获取锁 mutex.acquire(); //进行业务流程 System.out.println("Enter mutex"); //完成业务流程, 释放锁 mutex.release(); //关闭客户端 client.close(); }
锁的获取和释放使用 acquire()和release()方法即可
下面来分析下获取锁的源码实现。
acquire的方法如下:
/* * 获取锁,当锁被占用时会阻塞等待,这个操作支持同线程的可重入(也就是重复获取锁),acquire的次数需要与release的次数相同。 * @throws Exception ZK errors, connection interruptions */ @Override public void acquire() throws Exception { if ( !internalLock(-1, null) ) { throw new IOException("Lost connection while trying to acquire lock: " + basePath); } }
这里有个地方需要注意,当与zookeeper通信存在异常时,acquire会直接抛出异常,需要使用者自身做重试策略。代码中调用了internalLock(-1, null),参数表明在锁被占用时永久阻塞等待。internalLock的代码如下:
private boolean internalLock(long time, TimeUnit unit) throws Exception { //这里处理同线程的可重入性,如果已经获得锁,那么只是在对应的数据结构中增加acquire的次数统计,直接返回成功 Thread currentThread = Thread.currentThread(); LockData lockData = threadData.get(currentThread); if ( lockData != null ) { // re-entering lockData.lockCount.incrementAndGet(); return true; } //这里才真正去zookeeper中获取锁 String lockPath = internals.attemptLock(time, unit, getLockNodeBytes()); if ( lockPath != null ) { //获得锁之后,记录当前的线程获得锁的信息,在重入时只需在LockData中增加次数统计即可 LockData newLockData = new LockData(currentThread, lockPath); threadData.put(currentThread, newLockData); return true; } //在阻塞返回时仍然获取不到锁,这里上下文的处理隐含的意思为zookeeper通信异常 return false; }
zookeeper获取锁的具体实现:
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception { //参数初始化,此处省略
//...
//自旋获取锁 while ( !isDone ) { isDone = true; try { //在锁空间下创建临时且有序的子节点 ourPath = driver.createsTheLock(client, path, localLockNodeBytes); //判断是否获得锁(子节点序号最小),获得锁则直接返回,否则阻塞等待前一个子节点删除通知 hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath); }catch ( KeeperException.NoNodeException e ){ //对于NoNodeException,代码中确保了只有发生session过期才会在这里抛出NoNodeException,因此这里根据重试策略进行重试 if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) ) { isDone = false; }else{ throw e; }
} } //如果获得锁则返回该子节点的路径 if ( hasTheLock ) { return ourPath; } return null;
}
上面代码中主要有两步操作:
-
driver.createsTheLock:创建临时且有序的子节点,里面实现比较简单不做展开,主要关注几种节点的模式:
-
1)PERSISTENT(永久);
-
2)PERSISTENT_SEQUENTIAL(永久且有序);
-
3)EPHEMERAL(临时);
-
4)EPHEMERAL_SEQUENTIAL(临时且有序)。
-
internalLockLoop:阻塞等待直到获得锁。
internalLockLoop是怎么判断锁以及阻塞等待的,这里删除了一些无关代码,只保留主流程:
//自旋直至获得锁 while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) { //获取所有的子节点列表,并且按序号从小到大排序 List<String> children = getSortedChildren(); //根据序号判断当前子节点是否为最小子节点 String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases); if ( predicateResults.getsTheLock() ) { //如果为最小子节点则认为获得锁 haveTheLock = true; }else{ //否则获取前一个子节点 String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch(); //这里使用对象监视器做线程同步,当获取不到锁时监听前一个子节点删除消息并且进行wait(),当前一个子节点删除(也就是锁释放)时,回调会通过notifyAll唤醒此线程,此线程继续自旋判断是否获得锁 synchronized(this){ try{ //这里使用getData()接口而不是checkExists()是因为,如果前一个子节点已经被删除了那么会抛出异常而且不会设置事件监听器,而checkExists虽然也可以获取到节点是否存在的信息但是同时设置了监听器,这个监听器其实永远不会触发,对于zookeeper来说属于资源泄露 client.getData().usingWatcher(watcher).forPath(previousSequencePath); //如果设置了阻塞等待的时间 if ( millisToWait != null ) { millisToWait -= (System.currentTimeMillis() - startMillis); startMillis = System.currentTimeMillis(); if ( millisToWait <= 0 ) { doDelete = true; // 等待时间到达,删除对应的子节点 break; } //等待相应的时间 wait(millisToWait); }else{ //永远等待 wait(); } }catch ( KeeperException.NoNodeException e ){ //上面使用getData来设置监听器时,如果前一个子节点已经被删除那么会抛出NoNodeException,只需要自旋一次即可,无需额外处理 } } } }