curator 分布式锁InterProcessMutex

写这篇文章的目的主要是为了记录下自己在zookeeper 锁上踩过的坑,以及踩坑之后自己的一点认识;



从zk分布式锁原理说起,原理很简单,大家也应该都知道,简单的说就是zookeeper实现分布式锁是通过在zk集群上的路径实现的,在获取分布式锁的时候在zk服务器集群节点上创建临时顺序节点,释放锁的时候删除该临时节点.
多么简单的一句话,但是当你实现起来,想去做点优化的时候往往会变得很难,难的我们后续说;



再从需求说起,需求就是加锁,但是由于原来吞吐量不是很大,只是配置了一个固定的锁路径,但是却不是每次都会去根据这个锁路径创建锁,而是将这个锁路径存放在一个本地的HashMap中,这样的话,我就没有必要每次都去重复的创建这个锁对象,简单高效的利用;



变更后的需求是这样的,为了降低锁的力度,每次我要动态的生成一个path去zk上进行创建,然后再根据这个path生成锁对象,但是,一开始我依旧是沿用老的思维,想避免重复创建这个path的锁对象,于是,我想弄个三方缓存来存储这个锁对象,这时候坑就来了;

接下来,我们开始分析我的踩坑之旅:

public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex>
{
...
}

这是curator里面重入锁对象的结构,InterProcessLock这个是curator通用的锁接口,定义的跟jdk本身的也差不多,也是curator留给开发者自己去定制实现符合自己业务需求的锁对象的;Revocable接口是用来执行取消动作时触发动作用到的,如果你自定义锁对象的时候在释放锁对象时想触发一些动作,你可以实现它的方法,以上便是InterProcessLock结构的介绍;

看到这个代码结构我们还看出什么东西没?它并没有实现Serializable,导致其无法被序列化,也就是上面我自己想改进我业务中锁的场景就不支持了,因为类似于redis这种缓存,没法去存放一个对象,它顶多支持字符串以及byte[],所以我的想法就被loss掉了;

即使与业务无关了,但是我们作为可爱的程序员还是有必要去研究一下这个玩意的内部实现,因为我们不知道下次我们还会遇到什么场景,所以有必要让自己刻骨铭心一次;

接下来,我们看其内部实现,也就是我们高大上的源码之旅:

    private final LockInternals internals;
    private final String basePath;

    private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();

internals:这个是所有申请锁与释放锁的核心实现,待会我们再来讲内部实现;
basePath:锁定的路径;
threadData:内部缓存锁的容器;

实现流程主要是这样的:每次初始化InterProcessMutex对象的时候都会初始化一个StandardLockInternalsDriver对象,这个对象我们后面再讲它的使用,同时也会初始化一个LockInternals对象,

接下来,我们来看获取锁的代码:

public void acquire() throws Exception{
        if ( !internalLock(-1, null) ) {
            throw new IOException("Lost connection while trying to acquire lock: " + basePath);
        }
    }
private boolean internalLock(long time, TimeUnit unit) throws Exception {
        Thread currentThread = Thread.currentThread();
        LockData lockData = threadData.get(currentThread);
        if ( lockData != null )
        {
            // re-entering
            lockData.lockCount.incrementAndGet();
            return true;
        }

        String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
        if ( lockPath != null )
        {
            LockData newLockData = new LockData(currentThread, lockPath);
            threadData.put(currentThread, newLockData);
            return true;
        }

        return false;
    }

逻辑如下:
每次获取锁时会直接从本地缓存中先获取锁的元数据,如果存在,则在原有的计数器基础上+1,直接返回;
否则,尝试去获取锁,逻辑如下,

 String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
    {

        final long  startMillis = System.currentTimeMillis();
         //等待时间
        final Long   millisToWait = (unit != null) ? unit.toMillis(time) : null;
        final byte[]    localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
        int             retryCount = 0;

        String          ourPath = null;
        boolean         hasTheLock = false;
        boolean         isDone = false;
        while ( !isDone )
        {
            isDone = true;

            try
            {

                ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
                hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
            }
            catch ( KeeperException.NoNodeException e )
            {
                // gets thrown by StandardLockInternalsDriver when it can‘t find the lock node
                // this can happen when the session expires, etc. So, if the retry allows, just try it all again
                if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
                {
                    isDone = false;
                }
                else
                {
                    throw e;
                }
            }
        }

        if ( hasTheLock )
        {
            return ourPath;
        }

        return null;
    }

首先设置一个是否有锁的标志hasTheLock = false,然后
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);这个地方主要是通过StandardLockInternalsDriver在锁目录下创建EPHEMERAL_SEQUENTIAL节点,
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);这里主要是循环获取锁的过程,代码看下面,首先是判断是否实现了revocable接口,如果实现了那么就对这个path设置监听,否则的话通过StandardLockInternalsDriver尝试得到PredicateResults(主要是否得到锁及需要监视的目录的两个属性);

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).forPath(ourPath);
            }
            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();

                    synchronized(this)
                    {
                        try
                        {
                            // use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak
                            client.getData().usingWatcher(watcher).forPath(previousSequencePath);
                            if ( millisToWait != null )
                            {
                                millisToWait -= (System.currentTimeMillis() - startMillis);
                                startMillis = System.currentTimeMillis();
                                if ( millisToWait <= 0 )
                                {
                                    doDelete = true;    // timed out - delete our node
                                    break;
                                }

                                wait(millisToWait);
                            }
                            else
                            {
                                wait();
                            }
                        }
                        catch ( KeeperException.NoNodeException e )
                        {
                            // it has been deleted (i.e. lock released). Try to acquire again
                        }
                    }
                }
            }
        }
        catch ( Exception e )
        {
            ThreadUtils.checkInterrupted(e);
            doDelete = true;
            throw e;
        }
        finally
        {
            if ( doDelete )
            {
                deleteOurPath(ourPath);
            }
        }
        return haveTheLock;
    }

然后判断PredicateResults中的pathToWatch(主要保存sequenceNode)是否是最小的节点,如果是,则得到锁,getsTheLock为true,否则得到该序列的前一个节点,设为pathToWatch,并监控起来;再判断获取锁的时间是否超时,超时则删除节点,不竞争下次锁,否则,睡眠等待获取锁;最后把获取的锁对象的锁路径等信息封装成LockData存储在本地缓存中.

获取锁的逻辑主要就是这些,有兴趣的同学可以打断点跟踪学习下,



下面是释放锁的过程;

public void release() throws Exception
    {
        /*
            Note on concurrency: a given lockData instance
            can be only acted on by a single thread so locking isn‘t necessary
         */

        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 )
        {
            return;
        }
        if ( newLockCount < 0 )
        {
            throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
        }
        try
        {
            internals.releaseLock(lockData.lockPath);
        }
        finally
        {
            threadData.remove(currentThread);
        }
    }

代码很简单,从本地缓存中拿到锁对象,计数器-1,只有到那个计数器=0的时候才会去执internals.releaseLock(lockData.lockPath);

final void releaseLock(String lockPath) throws Exception
    {
        client.removeWatchers();
        revocable.set(null);
        deleteOurPath(lockPath);
    }

只要逻辑见名知意,首先移除watcher监听,这个监听可能是在循环获取锁的时候创建的,然后取消动作时触发动作时间置空,最后就是删除path;

最后做个小总结吧

    1. curator的InterProcessLock接口提供了多种锁机制,互斥锁,读写锁,以及可定时数的互斥锁的机制(这个大家具体问题具体分析).
    1. 所有申请锁都会创建临时顺序节点,保证了都能够有机会去获取锁.
    1. 内部用了线程的wait()和notifyAll()这种等待机制,可以及时的唤醒最渴望得到锁的线程.避免常规利用Thread.sleep()这种无用的间隔等待机制.
    1. 利用redis做锁的时候,一般都需要做锁的有效时间限定。而curator则利用了zookeeper的临时顺序节点特性,一旦客户端失去连接后,则就会自动清除该节点.

转自:https://www.jianshu.com/p/5fa6a1464076

原文地址:https://www.cnblogs.com/xifenglou/p/10497407.html

时间: 2024-10-15 04:42:54

curator 分布式锁InterProcessMutex的相关文章

06.Curator分布式锁

锁:分布式的锁全局同步,这意味着任何一个时间点不会有两个客户端都拥有相同的锁. 1.可重入锁Shared Reentrant Lock 首先我们先看一个全局可重入的锁(可以多次获取,不会被阻塞).Shared意味着锁是全局可见的,客户端都可以请求锁.Reentrant和JDK的ReentrantLock类似,意味着同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞. 1.可重入锁相关类介绍 它是由类InterProcessMutex来实现.它的主要方法: // 构造方法 public Inte

五、Curator使用:分布式锁

分布式锁介绍 分布式执行一些不需要同时执行的复杂任务,curator利用zk的特质,实现了这个选举过程.其实就是利用了多个zk客户端在同一个位置建节点,只会有一个客户端建立成功这个特性.来实现同一时间,只会选择一个客户端执行任务 代码 //分布式锁 InterProcessMutex lock = new InterProcessMutex(cc,"/lock_path"); CountDownLatch down = new CountDownLatch(1); for (int i

使用ZooKeeper实现Java跨JVM的分布式锁(优化构思)

说明:这篇文章是基于 使用ZooKeeper实现Java跨JVM的分布式锁 的,没有阅读的朋友请先阅读前面的文章后在阅读本文. 上一篇文章中介绍了如何使用分布式锁,并且对原来的公平锁进行了扩展,实现了非公平锁,已经能够满足大部分跨进程(JVM)锁的需求了. 问题:我们都知道在单个JVM内部实现锁的机制很方便,Java也提供了很丰富的API可以实现,例如Synchronized关键字, ReentrantLock等等,但是在集群环境中,都是多个JVM协同工作,当需要一些全局锁时就要用到上面介绍的分

SpringBoot电商项目实战 — Zookeeper的分布式锁实现

上一篇演示了基于Redis的Redisson分布式锁实现,那今天我要再来说说基于Zookeeper的分布式现实. Zookeeper分布式锁实现 要用Zookeeper实现分布式锁,我就不得不说说zookeeper的数据存储.首先zookeeper的核心保存结构是一个DataTree数据结构,其实内部是一个Map<String, DataNode> nodes的数据结构,其中key是path,DataNode才是真正保存数据的核心数据结构,DataNode核心字段包括byte data[]用于

【分布式锁】06-Zookeeper实现分布式锁:可重入锁源码分析

前言 前面已经讲解了Redis的客户端Redission是怎么实现分布式锁的,大多都深入到源码级别. 在分布式系统中,常见的分布式锁实现方案还有Zookeeper,接下来会深入研究Zookeeper是如何来实现分布式锁的. Zookeeper初识 文件系统 Zookeeper维护一个类似文件系统的数据结构 image.png 每个子目录项如NameService都被称为znoed,和文件系统一样,我们能够自由的增加.删除znode,在znode下增加.删除子znode,唯一不同的在于znode是

【zookeeper】Apache curator的使用及zk分布式锁实现

上篇,本篇主要讲Apache开源的curator的使用,有了curator,利用Java对zookeeper的操作变得极度便捷. 其实在学之前我也有个疑虑,我为啥要学curator,撇开涨薪这些外在的东西,就单技术层面来讲,学curator能帮我做些什么?这就不得不从zookeeper说起,上篇我已经大篇幅讲了zk是做什么的了,但真正要靠zk去实现多服务器自动拉取更新的配置文件等功能是非常难的,如果没有curator,直接去写的话基本上能把你累哭,就好比连Mybatis或者jpa都没有,让你用原

基于Zookeeper实现的分布式互斥锁 - InterProcessMutex

Curator是ZooKeeper的一个客户端框架,其中封装了分布式互斥锁的实现,最为常用的是InterProcessMutex,本文将对其进行代码剖析 简介 InterProcessMutex基于Zookeeper实现了分布式的公平可重入互斥锁,类似于单个JVM进程内的ReentrantLock(fair=true) 构造函数 1234567891011121314151617 // 最常用public InterProcessMutex(CuratorFramework client, St

分布式锁-curator实现

理论篇: Curator是Netflix开源的一套ZooKeeper客户端框架. Netflix在使用ZooKeeper的过程中发现ZooKeeper自带的客户端太底层, 应用方在使用的时候需要自己处理很多事情, 于是在它的基础上包装了一下, 提供了一套更好用的客户端框架. Netflix在用ZooKeeper的过程中遇到的问题, 我们也遇到了, 所以开始研究一下, 首先从他在github上的源码, wiki文档以及Netflix的技术blog入手. 看完官方的文档之后, 发现Curator主要

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

一.写在前面 之前写过一篇文章(<拜托,面试请不要再问我Redis分布式锁的实现原理>),给大家说了一下Redisson这个开源框架是如何实现Redis分布式锁原理的,这篇文章再给大家聊一下ZooKeeper实现分布式锁的原理. 同理,我是直接基于比较常用的Curator这个开源框架,聊一下这个框架对ZooKeeper(以下简称zk)分布式锁的实现. 一般除了大公司是自行封装分布式锁框架之外,建议大家用这些开源框架封装好的分布式锁实现,这是一个比较快捷省事儿的方式. 二.ZooKeeper分布