1、原生ZK方案
Zookeeper中有一种节点叫做顺序节点,假如我们在/lock/目录下创建节3个点,ZooKeeper集群会按照提起创建的顺序来创建节点,节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003。
ZooKeeper中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZooKeeper集群断开连接,则开节点自动被删除。
EPHEMERAL_SEQUENTIAL为临时顺序节点
实现分布式锁的基本逻辑:
释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可。
以下是流程图:
读写锁:读写锁的实现与互斥锁类似,不同的地方在于创建自节点时读锁和写锁要区分类型。例如读锁的前缀可以设置为read,写锁的前缀可以设置为write。创建读锁的时候,检查是否有编号小于自己的写锁存在,若存在则对编号刚好小于自己的写锁节点进行监听。创建写锁时,检查创建的节点编号是否为最小,如不是最小,则需要对编号刚好小于自己的节点进行监听(此时不区分读锁和写锁)
2、Curator方案
封装了zk的客户端,其分布式实现方式和上面的基本相同。同时还提供了不同的锁类型:
可重入锁:实现类为InterProcessMutex,将线程对象,节点,锁对象相关联。InterProcessMutex内部维护了一个使用线程为key,{thread,path}为值的map,所以对不同的线程和请求加锁的节点进行一一对应。提供方法acquire 和 release。
不可重入锁:实现类为InterProcessSemaphoreMutex,类似InterProcessMutex,只是没有维护线程的map。
可重入读写锁:类似JDK的ReentrantReadWriteLock
.一个读写锁管理一对相关的锁。 主要由两个类实现:
使用时首先创建一个InterProcessReadWriteLock
实例,然后再根据你的需求得到读锁或者写锁, 读写锁的类型是InterProcessLock
。
读写锁的实现与互斥锁类似,不同的地方在于创建自节点时读锁和写锁要区分类型。例如读锁的前缀可以设置为read,写锁的前缀可以设置为write。创建读锁的时候,检查是否有编号小于自己的写锁存在,若存在则对编号刚好小于自己的写锁节点进行监听。创建写锁时,检查创建的节点编号是否为最小,如不是最小,则需要对编号刚好小于自己的节点进行监听(此时不区分读锁和写锁)
还有信号量和多锁对象。
3、menagerie方案
menagerie基于Zookeeper实现了java.util.concurrent包的一个分布式版本。这个封装是更大粒度上对各种分布式一致性使用场景的抽象。其中最基础和常用的是一个分布式锁的实现:
org.menagerie.locks.ReentrantZkLock,通过ZooKeeper的全局有序的特性和EPHEMERAL_SEQUENTIAL类型znode的支持,实现了分布式锁。
最常见互斥锁方案:
设计思路和Medis类似,但实现略有不同。
美团维护的Tair中增加了expireLock和expireUnlock接口,通过锁状态和过期时间戳来共同判断锁是否存在:只有锁已经存在且没有过期的状态才判定为有锁状态。在有锁状态下,不能加锁,能通过大于过期时间的时间戳进行解锁;在无锁状态下,可以加锁,加锁成功会返回过期时间戳,用于解锁使用。重要的是,expireLock的原子性可以保证加锁和解锁时不会因为线程抢占引起错误。
不可重入锁:在加锁时调用expireLock,解锁时调用expireUnlock接口。传入的参数为过期时间或者过期时间戳。可以防止当线程拿到锁之后阻塞或者宕机,锁可以在过期之后释放出来。同时可以满足解锁动作安全,当自己的锁过期时不会误删别人的锁。
可重入锁:类似不可重入锁,维护类似zk的一个线程数和锁名的map。
可重入读写锁:
读线程:先用当前时间进行一次解锁expireUnlock,如果能解开则说明没有线程在写,可以进行读操作,同时incr,将计数器加1;完成读之后进行decr。
写线程:getCount读取计数器,如果为0,则说明没有线程在读,否则则需要等待;再expireLock,如果成功说明获取到了写锁,否则则说明已经有线程在写了;完成写之后进行解锁expireUnlock
缺陷:均有两步操作,但无法保证原子性。