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

这篇具有很好参考价值的文章主要介绍了【分布式锁】06-Zookeeper实现分布式锁:可重入锁源码分析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

前面已经讲解了Redis的客户端Redission是怎么实现分布式锁的,大多都深入到源码级别。

在分布式系统中,常见的分布式锁实现方案还有Zookeeper,接下来会深入研究Zookeeper是如何来实现分布式锁的。

Zookeeper初识

文件系统

Zookeeper维护一个类似文件系统的数据结构

image.png

每个子目录项如NameService都被称为znoed,和文件系统一样,我们能够自由的增加、删除znode,在znode下增加、删除子znode,唯一不同的在于znode是可以存储数据的。

有4种类型的znode

  • PERSISTENT–持久化目录节点客户端与zookeeper断开连接后,该节点依旧存在

  • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

  • EPHEMERAL-临时目录节点客户端与zookeeper断开连接后,该节点被删除

  • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

通知机制

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)等,zookeeper会通知客户端。

分布式锁

有了zookeeper的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。

  1. 对于第一类,我们将zookeeper上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。厕所有言:来也冲冲,去也冲冲,用完删除掉自己创建的distribute_lock 节点就释放出锁。

  2. 对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除自己创建的znode节点。

image.png

注明:以上内容参考 https://www.cnblogs.com/dream-to-pku/p/9513188.html

Curator框架初识

Curator是Netflix公司开源的一套Zookeeper客户端框架。目前已经作为Apache的顶级项目出现,是最流行的Zookeeper客户端之一。

我们看下Apache Curator官网的介绍:

image.png

接着看下quick start中关于分布式锁相关的内容
地址为:http://curator.apache.org/getting-started.html

InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) 
{
    try 
    {
        // do some work inside of the critical section here
    }
    finally
    {
        lock.release();
    }
}

使用很简单,使用InterProcessMutex类,使用其中的acquire()方法,就可以获取一个分布式锁了。

Curator分布式锁使用示例

启动两个线程t1和t2去争夺锁,拿到锁的线程会占用5秒。运行多次可以观察到,有时是t1先拿到锁而t2等待,有时又会反过来。Curator会用我们提供的lock路径的结点作为全局锁,这个结点的数据类似这种格式:[_c_64e0811f-9475-44ca-aa36-c1db65ae5350-lock-00000000001],每次获得锁时会生成这种串,释放锁时清空数据。

接下来看看加锁的示例:

public class Application {
    private static final String ZK_ADDRESS = "192.20.38.58:2181";
    private static final String ZK_LOCK_PATH = "/locks/lock_01";

    public static void main(String[] args) throws InterruptedException {
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                ZK_ADDRESS,
                new RetryNTimes(10, 5000)
        );
        client.start();
        System.out.println("zk client start successfully!");

        Thread t1 = new Thread(() -> {
            doWithLock(client);
        }, "t1");
        Thread t2 = new Thread(() -> {
            doWithLock(client);
        }, "t2");

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

    private static void doWithLock(CuratorFramework client) {
        InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
        try {
            if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName() + " hold lock");
                Thread.sleep(5000L);
                System.out.println(Thread.currentThread().getName() + " release lock");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                lock.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

image.png

Curator 加锁实现原理

直接看Curator加锁的代码:

public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex> {

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

     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;
        }
    }

    @Override
    public boolean acquire(long time, TimeUnit unit) throws Exception
    {
        return internalLock(time, unit);
    }


     private boolean internalLock(long time, TimeUnit unit) 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 )
        {
            // 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;
    }   
}

直接看internalLock()方法,首先是获取当前线程,然后查看当前线程是否在一个concurrentHashMap中,这里是重入锁的实现,如果当前已经已经获取了锁,那么这个线程获取锁的次数再+1

如果没有获取锁,那么就是用attemptLock()方法去尝试获取锁,如果lockPath不为空,说明获取锁成功,并将当前线程放入到map中。

接下来看看核心的加锁逻辑attemptLock()方法:

入参:time : 获取锁等待的时间unit:时间单位lockNodeBytes:默认为null

public class LockInternals {    
    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
            {
                if ( localLockNodeBytes != null )
                {
                    ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, localLockNodeBytes);
                }
                else
                {
                    ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
                }
                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;
    }
}

ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);

使用的临时顺序节点,首先他是临时节点,如果当前这台机器如果自己宕机的话,他创建的这个临时节点就会自动消失,如果有获取锁的客户端宕机了,zk可以保证锁会自动释放的

创建的数据结构类似于:

客户端A获取锁的代码,生成的ourPath=xxxx01

客户端B获取锁的代码,生成的ourPath=xxxx02

查看Zookeeper中/locks/lock_01下所有临时节点数据:

PS:01/02的图没有截到,这里又跑了一次截图所示 03/04 的顺序节点在ZK中的显示

接着重点看interalLockLoop()的逻辑:

public class LockInternals {
    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)
                    {
                        Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
                        if ( stat != null )
                        {
                            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();
                            }
                        }
                    }
                    // else it may have been deleted (i.e. lock released). Try to acquire again
                }
            }
        }

           // 省略部分代码
        return haveTheLock;
    }
}

重点看while循环中的逻辑
首先是获取锁的逻辑:

  1. 获取/locks/lock_01下排好序的znode节点,上面看图已经知道,会有xxx01xxx02两个节点
  2. 调用getsTheLock()方法获取锁,其中maxLeases为1,默认只能一个线程获取锁
  3. 定位到StandardLockInternalsDriver.getsTheLock()方法,其中代码核心如下:int ourIndex = children.indexOf(sequenceNodeName);boolean getsTheLock = ourIndex &lt; maxLeases;
  4. 上面sequenceNodeName参数为xxx01的全路径名,然后查看在排好序的children列表中是否为第一个元素,如果是第一个元素,返回的ourIndex=0,此时则认为获取锁成功
  5. 如果为有序列表中的第一个元素,那么predicateResults.getsTheLock() 为true,获取锁的标志位havaTheLock为true,直接返回获取锁成功

然后是获取锁失败的逻辑:
获取锁失败的核心代码:

String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();

synchronized(this)
{
    Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
    if ( stat != null )
    {
        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();
        }
    }
}
  1. 针对上一个节点添加监听器
  2. 如果加锁有过期时间,到了过期时间后直接break退出循环
  3. 当前线程处于wait()状态,等待上一个线程释放锁

Curator 释放锁实现原理

释放锁其实很简单,直接删除当前临时节点,因为下一个节点监听了上一个节点信息,所以上一个节点删除后,当前节点就会被唤醒重新获取锁。

private void deleteOurPath(String ourPath) throws Exception
{
    try
    {
        client.delete().guaranteed().forPath(ourPath);
    }
    catch ( KeeperException.NoNodeException e )
    {
        // ignore - already deleted (possibly expired session, etc.)
    }
}

总结

一张图总结:

04_Zookeeper分布式锁实现原理.jpg

原图可查看我的分享:
https://www.processon.com/view/link/5e80508de4b06b85300175d2

作者:一枝花算不算浪漫

原文地址:https://www.cnblogs.com/wang-meng/p/12593992.html文章来源地址https://www.toymoban.com/news/detail-834594.html

喜欢 0

到了这里,关于【分布式锁】06-Zookeeper实现分布式锁:可重入锁源码分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Redisson 分布式锁可重入的原理

    目录 1. 使用 Redis 实现分布式锁存在的问题 2. Redisson 的分布式锁解决不可重入问题的原理 不可重入:同一个线程无法两次 / 多次获取锁 举例 method1 执行需要获取锁 method2 执行也需要(同一把)锁 如果 method1 中调用了 method2,就会出现死锁的情况 method1 执行的过程是同一个线

    2024年01月25日
    浏览(31)
  • 【面试 分布式锁详细解析】续命 自旋锁 看门狗 重入锁,加锁 续命 解锁 核心源码,lua脚本解析,具体代码和lua脚本如何实现

    自己实现锁续命 在 controller 里开一个 线程 (可以为 守护线程) 每10秒,判断一个 这个 UUID是否存在,如果 存在,重置为 30秒。 如果不存在,守护线程 也结束。 基本的key value 基本的使用 setIfAbsent存在不设置 16384 Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。 R

    2023年04月09日
    浏览(30)
  • redis — redis cluster集群模式下如何实现批量可重入锁?

    一、redis cluster 集群版 在Redis 3.0版本以后,Redis发布了Redis Cluster。该集群主要支持搞并发和海量数据处理等优势,当 Redis 在集群模式下运行时,它处理数据存储的方式与作为单个实例运行时不同。这是因为它应该准备好跨多个节点分发数据,从而实现水平可扩展性。具体能力表

    2024年01月21日
    浏览(33)
  • Java入门-可重入锁

    什么是可重入锁? 当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁; 可重入锁案例 程序可重入加锁 A.class,没有发生死锁。 sychronized锁 运行结果 ReentrantLock 运行结果 如何保证可重入 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里

    2024年02月22日
    浏览(31)
  • java~理解可重入锁

    在Java中,可重入锁(Reentrant Lock)是一种同步机制,允许线程在持有锁的情况下再次获取该锁,而不会被自己所持有的锁所阻塞。也就是说,一个线程可以多次获得同一个锁,而不会出现死锁的情况。 可重入锁在多线程编程中非常有用,它允许线程在访问共享资源时多次获取

    2024年02月09日
    浏览(33)
  • 并发编程之可重入锁ReentrantLock

    大家都知道在并发编程中一般会用到多线程技术,多线程技术可以大大增加系统QPS/TPS。但是在一些特殊的业务场景下我们需要限制线程的并发数目,比如秒杀系统、多种商品金额叠加运算等等都是需要限制线程数量。特别是在分布式微服务架构,多线程同步问题尤为明显。一

    2023年04月25日
    浏览(27)
  • 【Java | 多线程】可重入锁的概念以及示例

    可重入锁(又名递归锁)是一种特殊类型的锁,它允许 同一个线程在获取锁后再次进入该锁保护的代码块或方法,而不需要重新获取锁 。 说白了,可重入锁的特点就是同一个线程可以多次获取同一个锁,而不会因为之前已经获取过锁而阻塞。 可重入锁的一个优点是可以一定

    2024年04月24日
    浏览(26)
  • JavaEE 初阶篇-深入了解 CAS 机制与12种锁的特征(如乐观锁和悲观锁、轻量级锁与重量级锁、自旋锁与挂起等待锁、可重入锁与不可重入锁等等)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 乐观锁与悲观锁概述         1.1 悲观锁(Pessimistic Locking)         1.2 乐观锁(Optimistic Locking)         1.3 区别与适用场景         2.0 轻量级锁与重量级锁概述         2.1 真正加

    2024年04月16日
    浏览(26)
  • 分布式锁解决方案_Zookeeper实现分布式锁

    提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 分布式锁解决方案_Zookeeper实现分布式锁 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 提示:这里可以添加本文要记录的大概内容: Zookeeper 是一个开源的分布式协调服务,它

    2024年02月03日
    浏览(29)
  • 分布式锁实现方案-基于zookeeper的分布式锁实现(原理与代码)

    目录 一、基于zookeeper的分布式锁 1.1 基于Zookeeper实现分布式锁的原理 1.1.1 分布式锁特性说明 1.1.1.1 特点分析 1.1.1.2 本质 1.1.2 Zookeeper 分布式锁实现原理 1.1.2.1 Zookeeper临时顺序节点特性 1.1.2.2 Zookeeper满足分布式锁基本要求 1.1.2.3 Watcher机制 1.1.2.3 总结 1.2 分布式锁流程说明 1.2.1

    2024年04月24日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包