2024年大数据最新图解curator如何实现zookeeper分布式锁_curator 锁(3)

一、前言

更多内容见Zookeeper专栏:https://blog.csdn.net/saintmm/category_11579394.html

至此,Zookeeper系列的内容已出:

1.zookeeper集群搭建
2. Zookeeper集群选举机制
3. Paxos算法解析
4. Zookeeper(curator)实现分布式锁案例

紧接着上一篇的内容,从源码层面来看curator是如何实现zookeeper分布式锁的?

二、curator分布式锁种类

curator提供了四种分布式锁,都实现自接口InterProcessLock

JAVA-doc:https://curator.apache.org/apidocs/org/apache/curator/framework/recipes/locks/package-summary.html

1> InterProcessMutex

  • 可重入排它锁,每成功加锁一次,就要解锁一次。

2> InterProcessSemaphoreMutex

  • 不可重入排他锁

3> InterProcessReadWriteLock

  • 可重入读写锁,读共享,写互斥;
  • 一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁
  • 这意味着写锁可以降级成读锁, 比如请求写锁 —>请求读锁—>释放读锁 —->释放写锁。

4> InterProcessMultiLock

  • 联锁, 将多个锁作为单个实体管理的容器;
  • 当调用acquire(), 所有的锁都会被acquire(),如果请求失败,所有的锁都会被release。 同样调用release时所有的锁都被release(失败被忽略)。

下面以可重入排他锁InterProcessMutex为例,展开讨论;

三、Zookeeper分布式锁概述

1、Zookeeper分布式锁实现思路

Zookeeper实现排他锁的设计思路如下:

  • zk用/lock节点作为分布式锁,当不同的客户端到zk竞争这把锁的时候,zk会按顺序给不同的客户端创建一个临时子节点,挂在作为分布式锁的节点下面。

  • 假设第一个来到的客户端为A,第二个来到的是B,分布式锁节点下挂的第一个节点就是A(/lock/_c_A),B(/lock/_c_B)紧跟着A,且B会监听着A的生命状态;

    • 这里B会先获取到/lock路径下所有的节点,发现自己的锁节点(/lock/_c_B)不在第一位,进而监听自己前一位的锁节点(/lock/_c_A)。
  • 当A释放锁后A节点会被删除;B监听到A被删除,B可以尝试获得分布式锁了。

    • 具体体现为:客户端B获取/lock下的所有子节点,并进行排序,判断排在最前面的是否为自己,如果自己的锁节点在第一位,代表取锁成功。
    • 如果还有C节点、D节点,他们都只会监听他们前一个节点,即:C监听B、D监听C。

2、Zookeeper分布式锁解决的问题

1> 锁无法释放?

  • 使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

2> 互斥阻塞锁?

  • 使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

3> 不可重入?

  • 使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

4> 单点问题?

  • 使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

3、Zookeeper分布式锁优缺点?

1> 优点?

  • 有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
  • zookeeper的锁天生是公平锁 根据创建临时节点的顺序。

2> 缺点?

  • 性能上不如使用缓存实现分布式锁。
    • 因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。
    • ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。
  • 加锁不管成功还是失败的第一步是先创建临时节点 这样如果加锁的过多 会对zookeeper的存储压力过大。

四、InterProcessMute实现分布式锁原理

InterProcessMute首先是一个互斥锁,其次是依赖Zookeeper临时顺序节点实现的分布式锁;对于锁而言,最重要的是保护临界区,让多个线程对临界区的访问互斥;InterProcessMute依赖Zookeeper临时顺序节点的有序性实现分布式环境下互斥,依赖JVM层面的synchronized实现节点监听的互斥(防止羊群效应)。

InterProcessMute的acquire()方法用于获取锁,release()方法用于释放锁。

以如下测试类为例,展开源码分析:

public class LockTest {
    public static void main(String[] args) {
        //重试策略,定义初试时间3s,重试3次
        ExponentialBackoffRetry exponentialBackoffRetry = new ExponentialBackoffRetry(3000, 3);

        //初始化客户端
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("127.0.0.1:2181")
                .sessionTimeoutMs(3000)
                .connectionTimeoutMs(3000)
                .retryPolicy(exponentialBackoffRetry)
                .build();
        // start()开始连接,没有此会报错
        client.start();
        //利用zookeeper的类似于文件系统的特性进行加锁 第二个参数指定锁的路径
        InterProcessMutex interProcessMutex = new InterProcessMutex(client, "/lock");

        try {
            //加锁
            interProcessMutex.acquire();
            System.out.println(Thread.currentThread().getName() + "获取锁成功");
            Thread.sleep(60\_000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                //释放锁
                interProcessMutex.release();
                System.out.println(Thread.currentThread().getName() + "释放锁成功");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

1、加锁流程(acquire()方法)

InterProcessMutex#acquire()方法:

在这里插入图片描述

acquire()方法中直接调用internalLock()方法以不加锁成功就一直等待的方式加锁;
如果加锁出现异常,则直接抛出IOException。

0)加锁流程图

在这里插入图片描述

1)internalLock()
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 )
    {
        // 可重入,lockCount +1;
        // 此处只在本地变量变化了,没发生任何网络请求;对比redisson的分布式锁可重入的实现是需要操作redis的
        lockData.lockCount.incrementAndGet();
        return true;
    }

    // 进行加锁,继续往里跟
    String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
    if ( lockPath != null )
    {
        // 加锁成功
        LockData newLockData = new LockData(currentThread, lockPath);
        // 放入map
        threadData.put(currentThread, newLockData);
        return true;
    }

    return false;
}

internalLock()方法有两个入参:long类型的time 和 TimeUnit类型的 unit 共同表示加锁的超时时间。

一个InterProcessMutex在同一个JVM中可以由多个线程共同操作,因为其可重入性体现在JVM的线程层面,所以其维护了一个Map类型的变量threadData

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

用于记录每个线程持有的锁信息;锁信息采用LockData表示;

LockData

LockData是InterProcessMutex的静态内部类,其仅有三个变量:持有锁的线程、锁路径、锁重入的次数;

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

internalLock()方法逻辑
  1. 根据当前线程从InterProcessMutex的threadData变量中获取当前线程持有的锁信息;
  • 如果已经持有锁,说明是JVM层面的锁重入,则直接对LockData.lockCount + 1,然后返回加锁成功。
  • 锁重入的过程是没有产生任何网络请求的;而Redisson分布式锁可重入的实现是需要每次都操作Redis的。
  1. 如果未持有锁,则尝试加锁;
  • 加锁逻辑体现在LockInternals#attemptLock()方法中;
  • 加锁成功,则将加锁的路径和当前线程一起封装为锁数据LockData,以线程为key,LockData为value,作为键值对加入到threadData中;并返回加锁成功
  • 加锁失败,则直接返回加锁失败。
2)LockInternals#attemptLock() --> 尝试加锁
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
    final long startMillis = System.currentTimeMillis();
    // 将时间统一格式化ms
    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;
}

attemptLock()方法有三个入参:long类型的time 和 TimeUnit类型的 unit 共同表示尝试加锁的超时时间,字节数组类型的lockNodeBytes表示锁路径对应的节点值。

通过InterProcessMutex#internalLock()方法进入到attemptLock()方法时,lockNodeBytes为null,即:不给锁路径对应的节点赋值。
在这里插入图片描述

尝试加锁逻辑:

  1. 首先尝试加锁是支持重试机制的;尝试加锁的返回值为加锁成功的锁路径,如果加锁未成功则返回null。
  2. 通过锁驱动器LockInternalsDriver直接创建Zookeeper的临时有序节点,并返回节点路径;
  • 具体逻辑体现在StandardLockInternalsDriver#createsTheLock()方法中;
  1. 判断节点路径是否为第一个节点,如果是,表明加锁成功;否则等待Watcher唤醒。
  • 具体逻辑体现在internalLockLoop()方法中;
1> StandardLockInternalsDriver#createsTheLock() --> 创建临时有序节点

为什么LockInternalsDriver接口的实现是StandardLockInternalsDriver?

  • 因为在LockInternals构造器被调用时,传入的LockInternalsDriver是StandardLockInternalsDriver。
    在这里插入图片描述

createsTheLock()方法:

@Override
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
    String ourPath;
    if ( lockNodeBytes != null )
    {
        ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL\_SEQUENTIAL).forPath(path, lockNodeBytes);
    }


![img](https://img-blog.csdnimg.cn/img_convert/8be7465d476ef56109aad56dd91ac25f.png)
![img](https://img-blog.csdnimg.cn/img_convert/26100ad321477352ad283d36077ded73.png)
![img](https://img-blog.csdnimg.cn/img_convert/69045cfac0173234614303a291865c02.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**

Protection().withMode(CreateMode.EPHEMERAL\_SEQUENTIAL).forPath(path, lockNodeBytes);
    }


[外链图片转存中...(img-ErRclo7D-1714660433775)]
[外链图片转存中...(img-EB4NJxXH-1714660433776)]
[外链图片转存中...(img-x2GvFDHw-1714660433776)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**

  • 26
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值