在多线程中,我们可以使用Synchronizeed或者Lock解决多线程情况下共享资源访问的问题,但是它们处理的范围是线程级别的。在分布式架构中多个进程对同一个共享资源进行访问也存在数据安全问题,这个时候也需要使用锁来解决,这就是分布式锁。因为ZooKeeper支持临时节点和节点唯一性使得使用ZooKeeper实现分布式锁成为可能。

使用ZooKeeper获取锁的时候,可以在/Locks节点下创建一个临时借点/lock。ZooKeeper基于同级节点的唯一性,使得只有一个客户端能创建成功,该客户端就获取到锁,其他没有获取到锁的可以监听Locks节点下的子节点的变更事件,以做出适当的逻辑。

在获取锁的时候,我们定义了/lock为临时节点,当客户端与ZooKeeper断开或者主动删除节点,相当于释放锁,监控/Locks节点的客户端收到节点变化之后可以再次发起创建/lock节点获取锁。

以上就是ZooKeeper作为分布式锁的原理,值得庆幸的是Java语言从来都不缺少轮子,前面讲述的Curator就包含了ZooKeeper分布式锁的应用。Curator为我们提供了几种类型的锁:分布式排它锁—InterProcessSemaphoreMutex;分布式可重入锁—InterProcessMutex;分布式读写锁—InterProcessReadWriteLock;多个锁作为单个实体管理的容器—InterProcessMultiLock:

如下我们创建一个ZooKeeper客户端来模拟ZooKeeper的分布式锁的过程,代码如下所示,每次返回一个新的ZooKeeper客户端实例:

public static CuratorFramework getFramework() {
    CuratorFramework framework = null;
    RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
    framework = CuratorFrameworkFactory.builder()
            .connectString(host)
            .sessionTimeoutMs(timeout)  // 会话超时时间
            .connectionTimeoutMs(timeout) // 连接超时时间
            .retryPolicy(retryPolicy)
            .build();
    framework.start();
    return framework;
}

InterProcessSemaphoreMutex是一种不可重入的排他锁,也称为互斥锁,也就意味着不管有多少线程当前最多有一个线程获取到锁,即便是同一个线程,这种情况下可能会造成死锁,测试代码如下所示:

//创建两个InterProcessSemaphoreMutex实例,分别代表分布式的两个进程
InterProcessLock lock = new InterProcessSemaphoreMutex(ClientUtil.getFramework(),"/lock");
InterProcessLock lock2 = new InterProcessSemaphoreMutex(ClientUtil.getFramework(),"/lock");
    //线程1代表一个进程
    new Thread(()->{
        try {
            //获取锁
            lock.acquire();
            System.out.println(Thread.currentThread().getName() +"获取到锁");
            Thread.sleep(10000);
        } catch (Exception e) {
            System.out.println(e);
        }finally {
            try {
                //释放锁
                lock.release();
            } catch (Exception e) {
                System.out.println(e);
            }
        }

    },"thread1").start();
    //线程二代表一个进程
    new Thread(()->{
        try {
            //获取锁
            lock.acquire();
            System.out.println(Thread.currentThread().getName() +"获取到锁");
            Thread.sleep(10000);
        } catch (Exception e) {
            System.out.println(e);
        }finally {
            try {
                //释放锁
                lock.release();
            } catch (Exception e) {
                System.out.println(e);
            }
        }

    },"thread2").start();
}

InterProcessMutex是一个分布式可重入锁,在同一个线程可以二次获取到锁,InterProcessMutex通过在zookeeper的某路径节点下创建临时序列节点来实现分布式锁,即每个线程(跨进程的线程)获取同一把锁前,都需要在同样的路径下创建一个节点,节点名字由uuid + 递增序列组成。而通过对比自身的序列数是否在所有子节点的第一位,来判断是否成功获取到了锁。当获取锁失败时,它会添加watcher来监听前一个节点的变动情况,然后进行等待状态。直到watcher的事件生效将自己唤醒,或者超时时间异常返回。测试代码如下:

//创建两个InterProcessMutex实例,分别代表分布式的两个进程
InterProcessLock lock = new InterProcessMutex(ClientUtil.getFramework(), "/lock");
InterProcessLock lock2 = new InterProcessMutex(ClientUtil.getFramework(), "/lock");
//线程1代表一个进程
new Thread(() -> {
    try {
        //获取锁
        lock.acquire();
        System.out.println(Thread.currentThread().getName() + "第一次获取到锁");
        Thread.sleep(5000);
        lock.acquire();
        System.out.println(Thread.currentThread().getName() + "第二次获取到锁");
        Thread.sleep(5000);
    } catch (Exception e) {
        System.out.println(e);
    } finally {
        try {
            //释放锁
            lock.release();
            System.out.println(Thread.currentThread().getName() + "第一次释放锁");
            lock.release();
            System.out.println(Thread.currentThread().getName() + "第二次释放锁");
        } catch (Exception e) {
            System.out.println(e);
        }
    }

}, "thread1").start();
//线程二代表一个进程
new Thread(() -> {
    try {
        //获取锁
        lock.acquire();
        System.out.println(Thread.currentThread().getName() + "第一次获取到锁");
        Thread.sleep(5000);
        lock.acquire();
        System.out.println(Thread.currentThread().getName() + "第二次获取到锁");
        Thread.sleep(5000);
    } catch (Exception e) {
        System.out.println(e);
    } finally {
        try {
            //释放锁
            lock.release();
            System.out.println(Thread.currentThread().getName() + "第一次释放锁");
            lock.release();
            System.out.println(Thread.currentThread().getName() + "第二次释放锁");
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}, "thread2").start();

InterProcessReadWriteLock是分布式读写锁,读锁和读锁不互斥,只要有写锁就互斥,代码如下所示:

//创建两个InterProcessReadWriteLock实例,分别代表分布式的两个读写
InterProcessReadWriteLock lock = new InterProcessReadWriteLock(ClientUtil.getFramework(), "/lock");
InterProcessReadWriteLock lock2 = new InterProcessReadWriteLock(ClientUtil.getFramework(), "/lock");
InterProcessLock readLock = lock.readLock();
InterProcessLock readLock2 = lock2.readLock();
InterProcessLock writeLock = lock.writeLock();
InterProcessLock writeLock2 = lock2.writeLock();

//线程1代表一个进程
new Thread(() -> {
    try {
        //获取锁
        readLock.acquire();
        System.out.println(Thread.currentThread().getName() + "获取到读锁");
        Thread.sleep(5000);
        readLock.release();
        System.out.println(Thread.currentThread().getName() + "释放读锁");
        writeLock.acquire();
        System.out.println(Thread.currentThread().getName() + "获取到写锁");
        Thread.sleep(5000);
    } catch (Exception e) {
        System.out.println(e);
    } finally {
        try {
            //释放锁
            writeLock.release();
            System.out.println(Thread.currentThread().getName() + "释放写锁");
        } catch (Exception e) {
            System.out.println(e);
        }
    }

}, "thread1").start();
//线程二代表一个进程
new Thread(() -> {
    try {
        //获取锁
        readLock2.acquire();
        System.out.println(Thread.currentThread().getName() + "获取到读锁");
        Thread.sleep(5000);
        readLock2.release();
        System.out.println(Thread.currentThread().getName() + "释放读锁");
        writeLock2.acquire();
        System.out.println(Thread.currentThread().getName() + "获取到写锁");
        Thread.sleep(5000);
    } catch (Exception e) {
        System.out.println(e);
    } finally {
        try {
            writeLock2.release();
            System.out.println(Thread.currentThread().getName() + "释放写锁");
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}, "thread2").start();

InterProcessMultiLock可以作为多个锁作为单个实体管理的容器,也就是使用别的锁的实例创建InterProcessMultiLock,只有当InterProcessMultiLock中的锁全部获取到,InterProcessMultiLock才算获取到锁,这个在我的开发生涯中还没遇到过,部分测试代码如下所示:

//分别创建两个锁,排他锁和可重入锁
InterProcessLock interProcessLock1 = new InterProcessMutex(ClientUtil.getFramework(), "/lock");
InterProcessLock interProcessLock2 = new InterProcessSemaphoreMutex(ClientUtil.getFramework(), "/lock");
//将上面创建的两个锁交给InterProcessMultiLock实例管理
InterProcessLock lock = new InterProcessMultiLock(Arrays.asList(interProcessLock1, interProcessLock2));

//线程1代表一个进程
new Thread(() -> {
    try {
        //获取锁
        lock.acquire();
        System.out.println(Thread.currentThread().getName() +"获取到锁");
        Thread.sleep(10000);
    } catch (Exception e) {
        System.out.println(e);
    }finally {
        try {
            //释放锁
            lock.release();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}, "thread1").start();
//线程二代表一个进程
new Thread(() -> {
    try {
        //获取锁
        lock.acquire();
        System.out.println(Thread.currentThread().getName() +"获取到锁");
        Thread.sleep(10000);
    } catch (Exception e) {
        System.out.println(e);
    }finally {
        try {
            //释放锁
            lock.release();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}, "thread2").start();

除了上面的分布式锁之外,Curator还提供了分布式信号量的用法,代码如下所示:

/创建分布式信号量实例
final InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(ClientUtil.getFramework(), "/lock", 1);
new Thread(()-> {
    {
        try {
            // 获取一个许可
            Lease lease = semaphore.acquire();
            System.out.println("获取读信号量===============");
            Thread.sleep(5 * 1000);
            semaphore.returnLease(lease);
            System.out.println("释放读信号量===============");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}).start();

new Thread(() -> {
    {
        try {
            // 获取一个许可
            Lease lease = semaphore.acquire();
            System.out.println("获取读信号量===============");
            Thread.sleep(5 * 1000);
            semaphore.returnLease(lease);
            System.out.println("释放读信号量===============");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}).start();