基于Zookeeper实现分布式锁(三)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014401141/article/details/84111187

实现分布式锁的方式非常多,zookeeper、redis、数据库等均可。

zookeeper的四种节点类型

1、持久化节点 :所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。

2、持久化顺序节点:这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。

3、临时节点:和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。

4、临时顺序节点:相对于临时节点而言,临时顺序节点比临时节点多了个有序,也就是说,没创建一个节点都会加上节点对应的序号,先创建成功,序号越小。

监视器(watcher)

当zookeeper创建一个节点时,会注册一个该节点的监视器,当节点状态发生改变时,watch会被触发,zooKeeper将会向客户端发送一条通知(就一条,因为watch只能被触发一次)。

1.共享锁和排他锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。分为排他锁和共享锁。

排他锁

排他锁(Exclusive Locks, 简称 X 锁),又称为写锁或独占锁,是一种基本的锁类型。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁。

共享锁

共享锁(Shared Locks,简称S锁),又称为读锁。允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

1.1 实现排他锁原理

简单地说就是多个客户端同时去竞争创建同一个临时子节点,Zookeeper能够保证只有一个客户端创建成功,那么这个创建成功的客户端就获得排他锁。正常情况下,这个客户端执行完业务逻辑会删除这个节点,也就是释放了锁。如果该客户端宕机了,那么这个临时节点会被自动删除,锁也会被释放。

1.2 共享锁原理

基本原理:

创建临时有序节点,每个线程均能创建节点成功,但是其序号不同,只有序号最小的可以拥有锁,其它线程只需要监听比自己序号小的节点状态即可

基本思路如下:

1、在你指定的节点下创建一个锁目录lock;

2、线程X进来获取锁在lock目录下,并创建临时有序节点;

3、线程A获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁;

4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应);

5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。

下面分别通过Curator和使用zookeeper原生API来实现
使用zookeeper原生方式来实现

1.DistributeLock

package com.alen.distributed.problem.lock.zk.javaapi;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.List;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 锁,提供获取锁和解锁的方法
 * <p>
 * 基本思路如下:
 * 1、在你指定的节点下创建一个锁目录lock;
 * 2、线程X进来获取锁在lock目录下,并创建临时有序节点;
 * 3、线程A获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁;
 * 4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应);
 * 5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。
 *
 * @author alen
 * @create 2018-11-15 15:58
 **/
public class DistributeLock {
    private static final Logger log = LoggerFactory.getLogger(DistributeLock.class);
    private static final String ROOT_LOCKS = "/LOCKS";//根节点
    private final static byte[] data = {1, 2}; //节点的数据
    private ZooKeeper zooKeeper;
    private int sessionTimeout; //会话超时时间
    private String lockID; //记录锁节点id
    private CountDownLatch countDownLatch = new CountDownLatch(1);
    static {
        try {
            ZKClient.getInstance().create(ROOT_LOCKS, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public DistributeLock() throws IOException, InterruptedException {
        this.zooKeeper = ZKClient.getInstance();
        this.sessionTimeout = ZKClient.getSessionTimeout();
    }

    //获取锁的方法
    public boolean lock() {
        try {
            //1.通步创建/LOCKS节点下开放权限的临时顺序节点
            lockID = zooKeeper.create(ROOT_LOCKS + "/", data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            log.info(Thread.currentThread().getName() + "->成功创建了lock节点[" + lockID + "], 开始去竞争锁");
            //获取根节点下的所有子节点
            List<String> childrenNodes = zooKeeper.getChildren(ROOT_LOCKS, true);
            //排序,从小到大
            SortedSet<String> sortedSet = new TreeSet<String>();
            for (String children : childrenNodes) {
                sortedSet.add(ROOT_LOCKS + "/" + children);
            }
            //2.拿到最小的节点
            String first = sortedSet.first();
            if (lockID.equals(first)) {
                //表示当前就是最小的节点
                log.info(Thread.currentThread().getName() + "->成功获得锁,lock节点为:[" + lockID + "]");
                return true;
            }
            SortedSet<String> lessThanLockId = sortedSet.headSet(lockID);//headSet(E e)//e之前的元素,不包括e
            //3.注册它上一个节点的监听
            if (!lessThanLockId.isEmpty()) {
                //拿到比当前LOCKID这个几点更小的上一个节点
                String prevLockID = lessThanLockId.last();
                //exists()方法用于监控节点变化,仅仅监控对应节点的一次数据变化,无论是数据修改还是删除!
                zooKeeper.exists(prevLockID, new LockWatcher(countDownLatch));
                countDownLatch.await(sessionTimeout, TimeUnit.MILLISECONDS);
                //上面这段代码意味着如果会话超时或者节点被删除(释放)了
                log.info(Thread.currentThread().getName() + " 成功获取锁:[" + lockID + "]");
            }
            return true;
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 解锁,删除节点
     * @return
     */
    public boolean unlock() {
        log.info(Thread.currentThread().getName() + "->开始释放锁:[" + lockID + "]");
        try {
            zooKeeper.delete(lockID, -1);
            log.info("节点[" + lockID + "]成功被删除");
            return true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
        return false;
    }


    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(10);
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                DistributeLock lock = null;
                try {
                    lock = new DistributeLock();
                    latch.countDown();
                    latch.await();
                    lock.lock();
                    Thread.sleep(random.nextInt(500));
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (lock != null) {
                        lock.unlock();
                    }
                }
            }).start();
        }
    }
}

2.zk客户端

package com.alen.distributed.problem.lock.zk.javaapi;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

/**
 * zk客户端
 *
 * @author alen
 * @create 2018-11-15 16:04
 **/
public class ZKClient {
    private final static String CONNECTSTRING="127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
    private static int sessionTimeout=5000;
    //获取连接
    public static ZooKeeper getInstance() {
        final CountDownLatch conectStatus=new CountDownLatch(1);
        ZooKeeper zooKeeper= null;
        try {
            zooKeeper = new ZooKeeper(CONNECTSTRING, sessionTimeout, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if(event.getState()== Event.KeeperState.SyncConnected){
                        conectStatus.countDown();
                    }
                }
            });
            conectStatus.await();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return zooKeeper;
    }

    public static int getSessionTimeout() {
        return sessionTimeout;
    }
}

3.watcher

package com.alen.distributed.problem.lock.zk.javaapi;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import java.util.concurrent.CountDownLatch;

/**
 * 当节点发生变化时,通过watcher机制,可以让客户端得到通知,
 * watcher需要实现org.apache.ZooKeeper.Watcher接口。
 * @author alen
 * @create 2018-11-15 16:07
 **/
public class LockWatcher implements Watcher {
    private CountDownLatch latch;
    public LockWatcher(CountDownLatch latch) {
        this.latch = latch;
    }
    @Override
    public void process(WatchedEvent watchedEvent) {
        if(watchedEvent.getType()== Event.EventType.NodeDeleted){
            latch.countDown();
        }
    }
}

基于Apache的开源客户端Curator来实现分布式锁。

1.Curator实现不可重入锁

package com.alen.distributed.problem.lock.zk.cutator.reentrant;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 不可重入锁
 * @author alen
 * @create 2018-11-15 22:14
 **/
public class ZKCuratorNOReentrantLock {
    private static final Logger log = LoggerFactory.getLogger(ZKCuratorNOReentrantLock.class);
    private InterProcessSemaphoreMutex lock;//不可重入锁
    private static String lockPAth = "/noreentrantlock/shareLock";
    private final FakeLimitedResource resource;
    private static CuratorFramework curatorFramework;
    private String clientName;
    //zookeeper集群地址
    public static final String ZOOKEEPERSTRING = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
    private static final int QTY = 5;
    private static final int REPETITIONS = QTY * 10;

    static {
        CuratorFramework client = CuratorFrameworkFactory.newClient(ZOOKEEPERSTRING, new ExponentialBackoffRetry(1000, 3));
        client.start();
        curatorFramework = client;
    }


    public ZKCuratorNOReentrantLock(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName) {
        this.resource = resource;
        this.clientName = clientName;
        this.lock = new InterProcessSemaphoreMutex(client, lockPath);
    }

   //不可重入锁只能获得一次
    public void doWork(long time, TimeUnit unit) throws Exception {
        if (!lock.acquire(time, unit)) {
            throw new IllegalStateException(clientName + " 不能得到互斥锁");
        }
        log.info(clientName + " 已获取到互斥锁");
        if (!lock.acquire(time, unit)) {
            throw new IllegalStateException(clientName + " 不能得到互斥锁");
        }
        log.info(clientName + " 再次获取到互斥锁");
        try {
            resource.use(); // 使用资源
            Thread.sleep(1000 * 1);
        } finally {
            log.info(clientName + " 释放互斥锁");
            lock.release(); // 总是在finally中释放
            lock.release(); // 获取锁几次 释放锁也要几次
        }
    }

    public static void main(String[] args) throws Exception {
        final FakeLimitedResource resource = new FakeLimitedResource();
        ExecutorService service = Executors.newFixedThreadPool(QTY);
        try {
            for (int i = 0; i < QTY; ++i) {
                final int index = i;
                Callable<Void> task = new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        try {
                            final ZKCuratorNOReentrantLock example = new ZKCuratorNOReentrantLock(curatorFramework, lockPAth, resource, "Client " + index);
                            for (int j = 0; j < REPETITIONS; ++j) {
                                example.doWork(10, TimeUnit.SECONDS);
                            }
                        } catch (Throwable e) {
                            e.printStackTrace();
                        }
                        return null;
                    }
                };
                service.submit(task);
            }
            service.shutdown();
            service.awaitTermination(10, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw e;
        }
    }

}

2.模拟竞争资源

package com.alen.distributed.problem.lock.zk.cutator.reentrant;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author alen
 * @create 2018-11-15 18:07
 **/
public class FakeLimitedResource {
    private final AtomicBoolean inUse = new AtomicBoolean(false);

    public void use() throws InterruptedException {
        // 真实环境中我们会在这里访问/维护一个共享的资源
        // 这个例子在使用锁的情况下不会非法并发异常IllegalStateException /
        // /但是在无锁的情况由于sleep了一段时间,很容易抛出异常
        if (!inUse.compareAndSet(false, true)) {
            throw new IllegalStateException("Needs to be used by one client at a time");
        }
        try {
            Thread.sleep((long) (3 * Math.random()));
        } finally {
            inUse.set(false);
        }
    }
}


2.Curator实现可重入锁

Curator内部是通过InterProcessMutex(可重入锁)来在zookeeper中创建临时有序节点实现的,之前说过,如果通过临时节点及watch机制实现锁的话,这种方式存在一个比较大的问题:所有取锁失败的进程都在等待、监听创建的节点释放,很容易发生"羊群效应",zookeeper的压力是比较大的,而临时有序节点就很好的避免了这个问题,Curator内部就是创建的临时有序节点。

package com.alen.distributed.problem.lock.zk.cutator.reentrant;

import com.alen.distributed.problem.lock.zk.javaapi.DistributeLock;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 基于zk用cutator客户端实现的可重入锁
 * @author alen
 * @create 2018-11-15 17:39
 **/
public class ZKCuratorReentrantLock {
    private static final Logger log = LoggerFactory.getLogger(DistributeLock.class);
    private   	InterProcessMutex  lock;//可重入锁实现类
    private static final String lockPAth = "/zk-curator-reentrantlock";
    private final FakeLimitedResource resource;
    private String clientName;
    private static CuratorFramework curatorFramework;
    private static final int QTY = 5;
    private static final int REPETITIONS = QTY * 10;
    //zookeeper集群地址
    public static final String ZOOKEEPERSTRING = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";

    static {
        CuratorFramework client = CuratorFrameworkFactory.newClient(ZOOKEEPERSTRING, new ExponentialBackoffRetry(1000, 3));
        client.start();
        curatorFramework = client;
    }
    
    public ZKCuratorReentrantLock(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName) {
        this.resource = resource;
        this.clientName = clientName;
        this.lock = new InterProcessMutex(client, lockPath);
    }

   //可重入锁可多次获得
    public void doWork(long time, TimeUnit unit)
            throws Exception {
        //通过acquire()获得锁,并提供超时机制:
        if (!lock.acquire(time, unit)) {
            throw new IllegalStateException(clientName + " 不能得到可重入锁");
        }
        log.info(clientName + " 已获取到可重入锁");
        if (!lock.acquire(time, unit)) {
            throw new IllegalStateException(clientName + " 不能得到可重入锁");
        }
        log.info(clientName + " 再次获取到可重入锁");
        try {
            resource.use(); // 使用资源
            Thread.sleep(1000 * 1);
        } finally {
            log.info(clientName + " 释放可重入锁");
            lock.release(); // 总是在finally中释放
            lock.release(); // 获取锁几次 释放锁也要几次
        }
    }
    
    public static void main(String[] args) throws Exception {
        final FakeLimitedResource resource = new FakeLimitedResource();
        ExecutorService service = Executors.newFixedThreadPool(QTY);
        try {
            for (int i = 0; i < QTY; ++i) {
                final int index = i;
                Callable<Void> task = new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        try {
                            final ZKCuratorReentrantLock example = new ZKCuratorReentrantLock(curatorFramework, lockPAth, resource, "Client " + index);
                            for (int j = 0; j < REPETITIONS; ++j) {
                                example.doWork(10, TimeUnit.SECONDS);
                            }
                        } catch (Throwable e) {
                            e.printStackTrace();
                        }
                        return null;
                    }
                };
                service.submit(task);
            }
            service.shutdown();
            service.awaitTermination(10, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw e;
        }
    }

}

3.可重入读写锁InterProcessReadWriteLock

类似JDK的ReentrantReadWriteLock。一个读写锁管理一对相关的锁。一个负责读操作,另外一个负责写操作。读操作在写锁没被使用时可同时由多个进程使用,而写锁在使用时不允许读(阻塞)。此锁是可重入的。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。这也意味着写锁可以降级成读锁, 比如请求写锁 --->读锁 ---->释放写锁。从读锁升级成写锁是不行的。
可重入读写锁相关类介绍    
可重入读写锁主要由两个类实现:InterProcessReadWriteLock、InterProcessMutex。使用时首先创建一个InterProcessReadWriteLock实例,然后再根据你的需求得到读锁或者写锁,读写锁的类型是InterProcessMutex。

实现

package com.alen.distributed.problem.lock.zk.cutator.reentrant;

import com.alen.distributed.problem.lock.zk.javaapi.DistributeLock;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 基于zk用curator实现可重入读写锁
 *
 * @author alen
 * @create 2018-11-15 23:41
 **/
public class ZKSharedReentrantReadWriteLock {
    private static final Logger log = LoggerFactory.getLogger(ZKSharedReentrantReadWriteLock.class);
    private final InterProcessReadWriteLock lock;
    private final InterProcessMutex readLock;
    private final InterProcessMutex writeLock;
    private static final String lockPAth = "/zk-SharedReentrantReadWrite-lock";
    private final FakeLimitedResource resource;
    private String clientName;
    private static CuratorFramework curatorFramework;
    private static final int QTY = 5;
    private static final int REPETITIONS = QTY * 10;
    //zookeeper集群地址
    public static final String ZOOKEEPERSTRING = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";

    static {
        CuratorFramework client = CuratorFrameworkFactory.newClient(ZOOKEEPERSTRING, new ExponentialBackoffRetry(1000, 3));
        client.start();
        curatorFramework = client;
    }

    public ZKSharedReentrantReadWriteLock(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName) {
        this.resource = resource;
        this.clientName = clientName;
        lock = new InterProcessReadWriteLock(client, lockPath);
        readLock = lock.readLock();
        writeLock = lock.writeLock();
    }


    public void doWork(long time, TimeUnit unit) throws Exception {
        // 注意只能先得到写锁再得到读锁,不能反过来!!!
        if (!writeLock.acquire(time, unit)) {
            throw new IllegalStateException(clientName + " 不能得到写锁");
        }
        System.out.println(clientName + " 已得到写锁");
        if (!readLock.acquire(time, unit)) {
            throw new IllegalStateException(clientName + " 不能得到读锁");
        }
        System.out.println(clientName + " 已得到读锁");
        try {
            resource.use(); // 使用资源
            Thread.sleep(1000 * 1);
        } finally {
            System.out.println(clientName + " 释放读写锁");
            readLock.release();
            writeLock.release();
        }
    }



    public static void main(String[] args) throws Exception {
        final FakeLimitedResource resource = new FakeLimitedResource();
        ExecutorService service = Executors.newFixedThreadPool(QTY);
        try {
            for (int i = 0; i < QTY; ++i) {
                final int index = i;
                Callable<Void> task = new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        try {
                            final ZKSharedReentrantReadWriteLock example = new ZKSharedReentrantReadWriteLock(curatorFramework, lockPAth, resource, "Client " + index);
                            for (int j = 0; j < REPETITIONS; ++j) {
                                example.doWork(10, TimeUnit.SECONDS);
                            }
                        } catch (Throwable e) {
                            e.printStackTrace();
                        }
                        return null;
                    }
                };
                service.submit(task);
            }
            service.shutdown();
            service.awaitTermination(10, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw e;
        }
    }

}

参考:

https://blog.csdn.net/u010889616/article/details/80209629

https://blog.csdn.net/qq_16681279/article/details/78061526

猜你喜欢

转载自blog.csdn.net/u014401141/article/details/84111187