zookeeper如何实现分布式锁

半世苍凉 提交于 2019-12-14 05:54:11

1.利用zk的特性

利用zookeeper的节点特性实现独占锁,就是同级节点的唯一性,多个进程往zookeeper的指定节点中创建一个节点名称相同的节点,只有一个成功,另一个创建失败;创建失败的节点通过zookeeper的watcher机制来监听这个子节点的变化,一旦子节点发生删除事件,则再次触发进程区写锁
在这里插入图片描述
这种实现方式简单,但是会产生"惊群效应",如果存在许多客户端在等待获取锁,当成功获取到锁的节点释放锁后,所有处于等待的客户端都会被唤醒,这个时候 zookeeper 在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会对 zookeeper 服务器的性能产生比较的影响。

2.利用有序节点实现分布式锁

我们可以通过有序节点来实现分布式锁,每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。这个的实现和前面单节点实现的差异性在于,每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到 watcher 事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致羊群效应,因为每个客户端只需要监控一个节点。

3.curator 分布式锁的基本使用

curator 对于锁这块做了一些封装,curator 提供了InterProcessMutex 这样一个 api。除了分布式锁之外,还提供了 leader 选举、分布式队列等常用的功能。
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁

package com.zookeeper.demo;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class Demo {
    public static void main(String[] args) {
        CuratorFramework
                curatorFramework=null;

        curatorFramework= CuratorFrameworkFactory.builder().

                connectString("192.168.133.132:12181").

                sessionTimeoutMs(500000).
                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.currentThread().getName()+"->尝试获取锁");
                try {
                    lock.acquire();

                    System.out.println(Thread.currentThread().getName()+"->获得锁成功");
                } catch (Exception e)
                {

                    e.printStackTrace();
                }
                try {

                    Thread.sleep(4000);
                    lock.release();

                    System.out.println(Thread.currentThread().getName()+"->释放锁成功");
                } catch (Exception e)
                {

                    e.printStackTrace();
                }
            },"t"+i).start();
        }
    } }

4.Curator 实现分布式锁的基本原理

//构造函数
public InterProcessMutex(CuratorFramework client, String path) {
// Zookeeper 利用 path 创建临时顺序节点,实现公平锁的核心
        this(client, path, new StandardLockInternalsDriver());
    }
public InterProcessMutex(CuratorFramework client, 
String path, LockInternalsDriver driver){
 // maxLeases=1,表示可以获得分布式锁的线程数量
(跨 JVM)为 1,即为互斥锁
 this(client, path, LOCK_NAME, 1, driver);
}

// protected 构造函数
InterProcessMutex(CuratorFramework client, String 
path, String lockName, int maxLeases, 
LockInternalsDriver driver){
 basePath = PathUtils.validatePath(path);
 // internals 的类型为 LockInternals ,
InterProcessMutex 将分布式锁的申请和释放操作委托给
internals 执行
 internals = new LockInternals(client, driver, path, 
lockName, maxLeases);
}

private boolean internalLock(long time, TimeUnit 
unit) throws Exception{
 Thread currentThread = 
Thread.currentThread();
 LockData lockData = 
threadData.get(currentThread);
 if ( lockData != null ){
 // 实现可重入
 // 同一线程再次 acquire,首先判断当前的
映射表内(threadData)是否有该线程的锁信息,如果有
则原子+1,然后返回
 lockData.lockCount.incrementAndGet();
 return true;
 }
 // 映射表内没有对应的锁信息,尝试通过
LockInternals 获取锁
 String lockPath = internals.attemptLock(time, 
unit, getLockNodeBytes());
 if ( lockPath != null ){
 // 成功获取锁,记录信息到映射表
 LockData newLockData = new 
LockData(currentThread, lockPath);
 threadData.put(currentThread, 
newLockData);
 return true;
 }
 return false;
}
// 映射表
// 记录线程与锁信息的映射关系
private final ConcurrentMap<Thread, LockData> 
threadData = Maps.newConcurrentMap();
// 锁信息
// Zookeeper 中一个临时顺序节点对应一个“锁”,但
让锁生效激活需要排队(公平锁),下面会继续分析
private static class LockData{
 final Thread owningThread;
 final String lockPath;
 final AtomicInteger lockCount = new 
AtomicInteger(1); // 分布式锁重入次数
 private LockData(Thread owningThread, 
String lockPath){
 this.owningThread = owningThread;
 this.lockPath = lockPath;
 } }
LockInternals.attemptLock
// 尝试获取锁,并返回锁对应的 Zookeeper 临时顺
序节点的路径
String attemptLock(long time, TimeUnit unit, byte[] 
lockNodeBytes) throws Exception{
 final long startMillis = 
System.currentTimeMillis();
 // 无限等待时,millisToWait 为 null
 final Long millisToWait = (unit != null) ? 
unit.toMillis(time) : null;
 // 创建 ZNode 节点时的数据内容,无关紧要,
这里为 null,采用默认值(IP 地址)
 final byte[] localLockNodeBytes = 
(revocable.get() != null) ? new byte[0] : lockNodeBytes;
 // 当前已经重试次数,与CuratorFramework的
重试策略有关
 int retryCount = 0;
 // 在 Zookeeper 中创建的临时顺序节点的路
径,相当于一把待激活的分布式锁
 // 激活条件:同级目录子节点,名称排序最小
(排队,公平锁),后续继续分析
 String ourPath = null;
 // 是否已经持有分布式锁
 boolean hasTheLock = false;
 // 是否已经完成尝试获取分布式锁的操作
 boolean isDone = false;
 while ( !isDone ){
 isDone = true;
 try{
 // 从 InterProcessMutex 的构造函数
可知实际 driver 为 StandardLockInternalsDriver 的实例
 // 在Zookeeper中创建临时顺序节点
 ourPath = 
driver.createsTheLock(client, path, 
localLockNodeBytes);
 // 循环等待来激活分布式锁,实现锁
的公平性,后续继续分析
 hasTheLock = 
internalLockLoop(startMillis, millisToWait, ourPath);
 } catch 
( KeeperException.NoNodeException e ) {
 // 容错处理,不影响主逻辑的理解,可
跳过
 // 因 为 会 话 过 期 等 原 因 ,
StandardLockInternalsDriver 因为无法找到创建的临时
顺序节点而抛出 NoNodeException 异常
 if 
( client.getZookeeperClient().getRetryPolicy().allowRetr
y(retryCount++,
 System.currentTimeMillis() -
startMillis, RetryLoop.getDefaultRetrySleeper()) ){
 // 满足重试策略尝试重新获取锁
 isDone = false;
 } else {
 // 不满足重试策略则继续抛出
NoNodeException
 throw e;
 }
 }
 }
 if ( hasTheLock ){
 // 成功获得分布式锁,返回临时顺序节点
的路径,上层将其封装成锁信息记录在映射表,方便锁重
入
 return ourPath;
 }
 // 获取分布式锁失败,返回 null
 return null;
}
createsTheLock
// From StandardLockInternalsDriver
// 在 Zookeeper 中创建临时顺序节点
public String createsTheLock(CuratorFramework 
client, String path, byte[] lockNodeBytes) throws 
Exception{
 String ourPath;
 // lockNodeBytes 不为 null 则作为数据节点内
容,否则采用默认内容(IP 地址)
 if ( lockNodeBytes != null ){
 // 下面对 CuratorFramework 的一些细节
做解释,不影响对分布式锁主逻辑的解释,可跳过
 // creatingParentContainersIfNeeded:用
于创建父节点,如果不支持 CreateMode.CONTAINER
 // 那么将采用 CreateMode.PERSISTENT
 // withProtection:临时子节点会添加GUID
前缀
 ourPath = 
client.create().creatingParentContainersIfNeeded()
 // 
CreateMode.EPHEMERAL_SEQUENTIAL:临时顺序节
点,Zookeeper 能保证在节点产生的顺序性
 // 依据顺序来激活分布式锁,从而也
实现了分布式锁的公平性,后续继续分析
 .withProtection().withMode(CreateM
ode.EPHEMERAL_SEQUENTIAL).forPath(path, 
lockNodeBytes);
 } else {
 ourPath = 
client.create().creatingParentContainersIfNeeded()
 .withProtection().withMode(CreateM
ode.EPHEMERAL_SEQUENTIAL).forPath(path);
 }
 return ourPath;
}
LockInternals.internalLockLoop
// 循环等待来激活分布式锁,实现锁的公平性
private boolean internalLockLoop(long startMillis, 
Long millisToWait, String ourPath) throws Exception {
 // 是否已经持有分布式锁
 boolean haveTheLock = false;
 // 是否需要删除子节点
 boolean doDelete = false;
 try {
 if (revocable.get() != null) {
 
client.getData().usingWatcher(revocableWatcher).forPa
th(ourPath);
 }
 while ((client.getState() == 
CuratorFrameworkState.STARTED) && !haveTheLock) {
 // 获取排序后的子节点列表
 List<String> children = 
getSortedChildren();
 // 获取前面自己创建的临时顺序子节
点的名称
 String sequenceNodeName = 
ourPath.substring(basePath.length() + 1);
 // 实现锁的公平性的核心逻辑,看下
面的分析
 PredicateResults predicateResults = 
driver.getsTheLock(client,
 
children , sequenceNodeName , maxLeases);
 if (predicateResults.getsTheLock()) {
 // 获得了锁,中断循环,继续返回
上层
 haveTheLock = true;
 } else {
 // 没有获得到锁,监听上一临时
顺序节点
 String previousSequencePath = 
basePath + "/" + predicateResults.getPathToWatch();
 synchronized (this) {
 try {
 // exists()会导致导致资
源泄漏,因此 exists()可以监听不存在的 ZNode,因此采
用 getData()
 // 上一临时顺序节点如
果被删除,会唤醒当前线程继续竞争锁,正常情况下能直
接获得锁,因为锁是公平的
 
client.getData().usingWatcher(watcher).forPath(previou
sSequencePath);
 if (millisToWait != null) {
 millisToWait -= 
(System.currentTimeMillis() - startMillis);
 startMillis = 
System.currentTimeMillis();
 if (millisToWait <= 
0) {
 doDelete = 
true; // 获取锁超时,标记删除之前创建的临时顺序节点
 break;
 }
 wait(millisToWait); 
// 等待被唤醒,限时等待
 } else {
 wait(); // 等待被唤
醒,无限等待
 }
 } catch 
(KeeperException.NoNodeException e) {
 // 容错处理,逻辑稍微有点
绕,可跳过,不影响主逻辑的理解
 // client.getData()可能调用
时抛出 NoNodeException,原因可能是锁被释放或会话
过期(连接丢失)等
 // 这里并没有做任何处理,
因为外层是 while 循环,再次执行 driver.getsTheLock 时
会调用 validateOurIndex
 // 此 时 会 抛 出
NoNodeException,从而进入下面的 catch 和 finally 逻
辑,重新抛出上层尝试重试获取锁并删除临时顺序节点
 }
 }
 }
 }
 } catch (Exception e) {
 ThreadUtils.checkInterrupted(e);
 // 标记删除,在 finally 删除之前创建的临
时顺序节点(后台不断尝试)
 doDelete = true;
 // 重新抛出,尝试重新获取锁
 throw e;
 } finally {
 if (doDelete) {
 deleteOurPath(ourPath);
 }
 }
 return haveTheLock;
}
getTheLock
// From StandardLockInternalsDriver
public PredicateResults 
getsTheLock(CuratorFramework client, List<String> 
children, String sequenceNodeName, int maxLeases) 
throws Exception{
 // 之前创建的临时顺序节点在排序后的子节点
列表中的索引
 int ourIndex = 
children.indexOf(sequenceNodeName);
 // 校验之前创建的临时顺序节点是否有效
 validateOurIndex(sequenceNodeName, 
ourIndex);
 // 锁公平性的核心逻辑
 // 由 InterProcessMutex 的构造函数可知,
maxLeases 为 1,即只有 ourIndex 为 0 时,线程才能持
有锁,或者说该线程创建的临时顺序节点激活了锁
 // Zookeeper 的临时顺序节点特性能保证跨多
个 JVM 的线程并发创建节点时的顺序性,越早创建临时
顺序节点成功的线程会更早地激活锁或获得锁
 boolean getsTheLock = ourIndex < 
maxLeases;
 // 如果已经获得了锁,则无需监听任何节点,
否则需要监听上一顺序节点(ourIndex-1)
 // 因 为 锁 是 公 平 的 , 因 此 无 需 监 听 除 了
(ourIndex-1)以外的所有节点,这是为了减少羊群效应,
非常巧妙的设计!!
 String pathToWatch = getsTheLock ? null : 
children.get(ourIndex - maxLeases);
 // 返回获取锁的结果,交由上层继续处理(添
加监听等操作)
 return new PredicateResults(pathToWatch, 
getsTheLock);
}
static void validateOurIndex(String 
sequenceNodeName, int ourIndex) throws 
KeeperException{
 if ( ourIndex < 0 ){
 // 容错处理,可跳过
 // 由于会话过期或连接丢失等原因,该线
程创建的临时顺序节点被 Zookeeper 服务端删除,往外
抛出 NoNodeException
 // 如果在重试策略允许范围内,则进行重
新尝试获取锁,这会重新重新生成临时顺序节点
 // 佩服 Curator 的作者将边界条件考虑得
如此周到!
 throw new 
KeeperException.NoNodeException("Sequential path 
not found: " + sequenceNodeName);
 } }
释放锁的逻辑
InterProcessMutex.release
public void release() throws Exception{
 Thread currentThread = 
Thread.currentThread();
 LockData lockData = 
threadData.get(currentThread);
 if ( lockData == null ){
 // 无法从映射表中获取锁信息,不持有锁
 throw new 
IllegalMonitorStateException("You do not own the lock: 
" + basePath);
 }
 int newLockCount = 
lockData.lockCount.decrementAndGet();
 if ( newLockCount > 0 ){
 // 锁是可重入的,初始值为 1,原子-1 到 0,锁才释放
 return;
 }
 if ( newLockCount < 0 ){
 // 理论上无法执行该路径
 throw new 
IllegalMonitorStateException("Lock count has gone 
negative for lock: " + basePath);
 }
 try{
 // lockData != null && newLockCount == 
0,释放锁资源
 internals.releaseLock(lockData.lockPath);
 } finally {
 // 最后从映射表中移除当前线程的锁信息
 threadData.remove(currentThread);
 } }
LockInternals.releaseLock
void releaseLock(String lockPath) throws 
Exception{
 revocable.set(null);
 // 删除临时顺序节点,只会触发后一顺序节点去
获取锁,理论上不存在竞争,只排队,非抢占,公平锁,
先到先得
 deleteOurPath(lockPath);
}
// Class:LockInternals
private void deleteOurPath(String ourPath) throws 
Exception{
 try{
 // 后台不断尝试删除
 
client.delete().guaranteed().forPath(ourPath);
 } catch ( KeeperException.NoNodeException 
e ) {
 // 已经删除(可能会话过期导致),不做处理
 // 实际使用 Curator-2.12.0 时,并不会抛
出该异常
 } }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!