zk实现一个分布式锁

最近在工作做中,使用到了redis的分布式锁,主要采用了其中一个原子操作getAndSet,并且利用了redis单线程的特性操作。这里来写另一种分布式的锁,zk实现的分布式锁。

zk的特性

1、数据结构
zk的数据结构,存储的方式为key-value,存储的结构为文件夹的结构。
在这里插入图片描述
所以由于是这样的结构,在同级目录下面不存在相同的节点

2、节点的特性

  • 有序节点
  • 持久化节点
  • 临时节点

3、watch的机制

在zk中,可以设置一些监听,来监听节点的一些变化操作,做出响应。
绑定监听:getData、exists、getChildren
触发监听:create、delete、setData
查操作可以绑定监听,增、删、改可以绑定监听,且监听是一次性的,在触发一次会消失,可以循环绑定来达到一直监听的效果。

4、leader的机制
这个机制,在本次的zk分布式锁中,没有作用,但是也是zk一个很重要的点。点击查看详情

方案预想

1、 利用在同级目录下,不能创建相同的节点特性,可以利用多线程去创建一个节点,但是只能有一个线程可以创建成功,所以该线程得到锁。释放锁:释放锁时,通过删除该节点,来触发刚才没有获取到的线程的监听,让他们再次来竞争获取。如图:
在这里插入图片描述

结论:可以达到效果,但是如果有1000个线程发起竞争,那么在释放锁时会999个线程触发监听,重新发起创建节点的请求。性能上不够优化。羊群效应,产生过多不必要的开销。

2、利用有序的节点特性,可以让多个线程同时创建多个有序的节点,其中创建的节点最小的线程,可以获取本次的锁,在释放锁以后,删除节点,自动为排序的下一个节点获取锁。如下图:
在这里插入图片描述

结论:在之前的文章中,我们分析过独占锁的源码,在独占锁中,用阻塞队列来存放线程,同时在公平锁模式下,会优先用队列的第一个线程来获取锁。这里,就很类似,当001被删除后,002 就会顺势获取锁,依次获取。

代码实现
public class DispathLock implements Lock,Watcher {

    private ZooKeeper zooKeeper;

    private String ROOTPATH = "/lock";

    private String current; //当前节点

    private String pre; //前一个节点


    public DispathLock() {
        try {
        //创建zk的链接
            zooKeeper = new ZooKeeper("127.0.0.1:2181",500,this);
            Stat stat = zooKeeper.exists(ROOTPATH,false);
            //判断是否存在 /lock 的节点,用来存放在锁竞争过程中的数据
            if(stat == null){
               //不存在 /lock 创建  zooKeeper.create(ROOTPATH,"0".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void lock() {

            if(tryLock()){
                System.out.println(Thread.currentThread().getName() +" 获取锁成功");
                return;
            }
            try {
                waitForLock();
            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }



    }
    //等待pre节点被删除 ==上一个获取锁的线程释放锁
    private void waitForLock() throws KeeperException, InterruptedException {
       Stat stat =  zooKeeper.exists( pre, true);
       if(stat !=null){
           System.out.println(Thread.currentThread().getName() + "正在等待");
           synchronized (pre){
               Long curentTime = System.currentTimeMillis();
                   pre.wait(10000); //设置超时时间
               Long now = System.currentTimeMillis();

                if(now -  curentTime>= 10000){
                    System.out.println(Thread.currentThread().getName()+"超过等待时间,退出竞争");
                    unlock();
                    return;
                }
               System.out.println(Thread.currentThread().getName() + "获取锁成功");
           }
       }


    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }
    @Override
    public boolean tryLock() {
        //创建临时有序的节点
        try {
            current =  zooKeeper.create(ROOTPATH+"/","0".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
            System.out.println(Thread.currentThread().getName() + "获取到的节点为"+current);
            List<String> list =  zooKeeper.getChildren(ROOTPATH,false); //当前所有的节点
            //对节点排序
            list =  list.stream().map(e -> {
                return ROOTPATH+"/"+e;
            }).sorted((a,b) -> {
                return a.compareTo(b);
            }).collect(Collectors.toList());
            //当前节点的序号
            int i =  list.indexOf(current);
            if(i == 0){
                return true; //拿到锁了
            }else{
                //记录上一个节点
                pre = list.get(i - 1) ;
                return false;
            }

        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
    @Override
    public void unlock() {
        System.out.println("释放锁");
        try {
            zooKeeper.delete(current,-1);
            current = null;
            zooKeeper.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
    @Override
    public Condition newCondition() {
        return null;
    }
    @Override
    public void process(WatchedEvent watchedEvent) {
    //由于监听的是上一个的节点,所以,当process被触发时,上一个线程释放了锁,所以本线程可以去获取锁了
        synchronized (pre){
            pre.notify();
        }
    }

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i = 0;i<10;i++){
            new Thread(() -> {
                try {
                    countDownLatch.await();
                    DispathLock dispathLock = new DispathLock();
                    dispathLock.lock(); //获取锁
                }catch (Exception e){
                    e.printStackTrace();
                }
            }).start();
            countDownLatch.countDown();
        }
    }

}

主要采用了,方案2中的计划,简单实现了分布式锁。但是其中,没有实现重入锁,并且代码中没有只实现了公平锁,没有实现非公平锁。我们下面来看一下Curator的实现

InterProcessMutex

Curator是ZooKeeper的一个客户端框架,其中封装了分布式互斥锁的实现,最为常用的是InterProcessMutex。
在curator中,提供了各种基于zk的工具,locks、atomic、cache等等。本节讲锁,所以看一下InterProcessMutex的简单使用:

public class CuratorDemo {

    public static void main(String[] args) {
        //1 重试策略:初试时间为1s 重试10次
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 10);
        //2 通过工厂创建连接
        CuratorFramework cf = CuratorFrameworkFactory.builder()
                .connectString("127.0.0.1:2181")
                .sessionTimeoutMs(5000)
                .retryPolicy(retryPolicy)
                .build();
        //3 开启连接
        cf.start();
        InterProcessMutex lock = new InterProcessMutex(cf,"/cur-lock");

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "尝试获取锁。。。");
                try {
                //可重入
                    lock.acquire();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "获得锁");

                try {
                    System.out.println(Thread.currentThread().getName() + "执行中。。。。。。。。。。");
                    Thread.sleep(1000 * 60 * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "执行完,释放锁");
                try {
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "释放成功");
            },"线程"+i).start();
        }
    }


}

InterProcessMutex 分析 ,可以看到在构造时,需要传入一个根节点,在获取锁的时候,会在这个根节点下面做一些操作。直接来看获取锁的方法:

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
    {
        /*
           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);
        //判断缓存中是否有线程,有的话则说明,本次是获取重入操作,基数+1 直接返回true,不用去竞争
        if ( lockData != null )
        {
            //重入锁 基数+1
            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;
    }
 //下面是真正去获取锁的操作,相比于自己的版本,多了重试的限制
 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
            {
            //从InterProcessMutex的构造函数可知实际driver为StandardLockInternalsDriver的实例
            // 在Zookeeper中创建临时顺序节点
                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;
    }
    

分为2块代码,下面为创建有序的临时节点

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);
        }
        else
        {
            ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
        }
        return ourPath;
    }

以下为公平锁的实现策略

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

如需要使用基于zk的一些工具,尽量使用一些Curator中提供的。希望看完的同学,可以仔细看下面的这篇,了解zk中的leader选举过程
点击查看详情

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值