1.1.2. 图解: zookeeper分布式锁的原理
理解了锁的原理后,就会发现,Zookeeper 天生就是一副分布式锁的胚子。
首先,Zookeeper的每一个节点,都是一个天然的顺序发号器。
在每一个节点下面创建子节点时,只要选择的创建类型是有序(EPHEMERAL_SEQUENTIAL 临时有序或者PERSISTENT_SEQUENTIAL 永久有序)类型,那么,新的子节点后面,会加上一个次序编号。这个次序编号,是上一个生成的次序编号加一
比如,创建一个用于发号的节点“/test/lock”,然后以他为父亲节点,可以在这个父节点下面创建相同前缀的子节点,假定相同的前缀为“/test/lock/seq-”,在创建子节点时,同时指明是有序类型。如果是第一个创建的子节点,那么生成的子节点为/test/lock/seq-0000000000,下一个节点则为/test/lock/seq-0000000001,依次类推,等等。
其次,Zookeeper节点的递增性,可以规定节点编号最小的那个获得锁。
一个zookeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程都会在这个节点下创建个临时顺序节点,由于序号的递增性,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。
第三,Zookeeper的节点监听机制,可以保障占有锁的方式有序而且高效。
每个线程抢占锁之前,先抢号创建自己的ZNode。同样,释放锁的时候,就需要删除抢号的Znode。抢号成功后,如果不是排号最小的节点,就处于等待通知的状态。等谁的通知呢?不需要其他人,只需要等前一个Znode 的通知就可以了。当前一个Znode 删除的时候,就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个,击鼓传花似的依次向后。
Zookeeper的节点监听机制,可以说能够非常完美的,实现这种击鼓传花似的信息传递。具体的方法是,每一个等通知的Znode节点,只需要监听linsten或者 watch 监视排号在自己前面那个,而且紧挨在自己前面的那个节点。 只要上一个节点被删除了,就进行再一次判断,看看自己是不是序号最小的那个节点,如果是,则获得锁。
为什么说Zookeeper的节点监听机制,可以说是非常完美呢?
一条龙式的首尾相接,后面监视前面,就不怕中间截断吗?比如,在分布式环境下,由于网络的原因,或者服务器挂了或则其他的原因,如果前面的那个节点没能被程序删除成功,后面的节点不就永远等待么?
其实,Zookeeper的内部机制,能保证后面的节点能够正常的监听到删除和获得锁。在创建取号节点的时候,尽量创建临时znode 节点而不是永久znode 节点,一旦这个 znode 的客户端与Zookeeper集群服务器失去联系,这个临时 znode 也将自动删除。排在它后面的那个节点,也能收到删除事件,从而获得锁。
说Zookeeper的节点监听机制,是非常完美的。还有一个原因。
Zookeeper这种首尾相接,后面监听前面的方式,可以避免羊群效应。所谓羊群效应就是每个节点挂掉,所有节点都去监听,然后做出反映,这样会给服务器带来巨大压力,所以有了临时顺序节点,当一个节点挂掉,只有它后面的那一个节点才做出反映。
1.1.3. 分布式锁的基本流程
接下来就是基于zookeeper,实现一下分布式锁。
首先定义了一个锁的接口,很简单,一个加锁方法,一个解锁方法。
/**
* create by 尼恩 @ 疯狂创客圈
**/
public interface Lock {
boolean lock() throws Exception;
boolean unlock();
}
使用zookeeper实现分布式锁的算法流程,大致如下:
(1)如果锁空间的根节点不存在,首先创建Znode根节点。这里假设为“/test/lock”。这个根节点,代表了一把分布式锁。
(2)客户端如果需要占用锁,则在“/test/lock”下创建临时的且有序的子节点。
这里,尽量使一个有意义的子节点前缀,比如“/test/lock/seq-”。则第一个客户端对应的子节点为“/test/lock/seq-000000000”,第二个为 “/test/lock/seq-000000001”,以此类推。
如果前缀为“/test/lock/”,则第一个客户端对应的子节点为“/test/lock/000000000”,第二个为 “/test/lock/000000001” ,以此类推,也非常直观。
(3)客户端如果需要占用锁,还需要判断,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点。如果是则认为获得锁,否则监听前一个Znode子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;
(4)获取锁后,开始处理业务流程。完成业务流程后,删除对应的子节点,完成释放锁的工作。以便后面的节点获得分布式锁。