zk 实现分布式锁

znode数据结构有如下特点:
1> 每个znode都有唯一路径标识,最顶层的znode为/,比如p_2这个znode的路径标识为/app1/p_2,znode只支持绝对路径,不支持相对路径,也不支持“.”和“..”
2> znode可以有子节点,并且每个znode可以存储数据。但zk是被设计用来协调管理服务的,因此znode里存储的都是一些小数据,而不是大容量的数据,数据容量一般在1M范围内。
3> znode的数据有版本号,可以用在并发访问场景中,用乐观锁机制实现数据的一致性
4> znode分为临时节点和永久节点,zk的客户端和服务器通信采用长连接的方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为session,如果znode是临时节点,当session失效(即客户端与服务器断开连接),znode会被服务器自动删除。
5> znode的节点名称可以自动编号,如果app1已经存在,再创建的话,将会自动命名为app2,这种节点称为序列节点。
6> znode可以被监控,包括这个节点中存储的数据被修改、子节点列表变化(删除或新增子节点)等,一旦变化,zk服务器会通过所有监控该节点的客户端,这是zk的核心特性,zk很多的功能都是基于这个特性实现的。



zkCli.sh脚本是Zookeeper安装包中自带的一个客户端,放在$ZK_HOME/bin目录下,本文ZK安装在/opt/zookeeper-3.4.9。



zkCli.sh客户端连接到ZK服务器的语法为:zkCli.sh -timeout 5000 -r -server ip:port
连接参数解释:
1> -timeout:表示客户端向zk服务器发送心跳的时间间隔,单位为毫秒。因为zk客户端与服务器的连接状态是通过心跳检测来维护的,如果在指定的时间间隔内,zk客户端没有向服务器发送心跳包,服务器则会断开与该客户端的连接。参数5000,表示zk客户端向服务器发送心跳的间隔为5秒。
2> -r:表示客户端以只读模式连接
3> -server:指定zk服务器的IP与端口,zk默认的客户端端口为2181



shell> cd /usr/local/zookeeper/bin
shell> ./zkCli.sh -timeout 5000 -server 127.0.0.1:2181

1、查询子节点列表
语法:ls path
2、创建节点



语法:create path [-s] [-e] data acl
path:节点路径
-s:指定该节点是一个序列节点,创建同名的节点时,会给节点自动加上编号
-e:指定该节点是一个临时节点,默认是永久节点。临时节点会在客户端与服务器断开连接时,zk会将其创建的所有临时节点全部删除
data:存储在节点中的数据
acl:设置子节点访问权限,默认所有人都可以对该节点进行读写操作
3、获取节点状态
每个节点都包含描述该节点的一些状态信息,比如:节点数据、版本号等。
语法:stat path [watch]
path:节点全路径
watch:监听节点状态变化
在ZK中,ZK客户端对服务器每一个数据节点的写操作,ZK会认为都是一次完整的事务操作,要么成功,要么失败,保证了数据的原子性。而每次事务都会分配一个唯一的事务id,以标识这次事务操作的数据信息。下面详细理解一下节点状态各个字段的含义:
cZxid:创建节点的事务id
ctime:创建节点的时间
mZxid:修改节点的事务id
mtime:修改节点的时间
pZxid:子节点列表最后一次修改的事务id。删除或添加子节点,不包含修改子节点的数据。
cversion:子节点的版本号,删除或添加子节点,版本号会自增
dataVersion:节点数据版本号,数据写入操作,版本号会递增
aclVersion:节点ACL权限版本,权限写入操作,版本号会递增
ephemeralOwner:临时节点创建时的事务id,如果节点是永久节点,则它的值为0
dataLength:节点数据长度(单位:byte),中文占3个byte
numChildren:子节点数量
4、获取节点数据
语法:get path [watch]
path:节点路径
watch:监听节点数据变化。如果其它客户端修改了该节点的数据,则会通知监听了该节点的所有客户端
5、设置节点数据
语法:set path data [version]
path:节点路径
data:节点数据
version:数据版本号(节点状态dataVersion的值)
6、查询子节点列表及状态信息
语法:ls2 path [watch]
path:节点路径
watch:是否监听子节点列表变化通知
和ls命令不一样的是,ls2不仅能查询节点的子节点列表,同时也能查询到节点的状态信息。
7、删除节点
语法:delete path [version]
path:节点路径
version:节点版本号(节点状态cversion的值),可选。如果传递了版本号,则必须保证和服务器的版本号一致,否则会报错:version No is not valid : 节点路径
注意:delete只能删除没有子节点的节点,否则会报错
8、删除节点(包括子节点)
语法:rmr path
path:节点路径
rmr会递归删除子节点,再删除节点本身
9、设置节点配额
节点可以存储数据,也可以创建子节点,但是如果不做控制,节点数据可以无限大,子节点数量也可以创建无数个,所以在有些场景下需要对节点的数据和子节点的数量需要做一些限制,zk为我们提供了setauota命令实现对子节点的限制功能。但是,zk并不是真正在的物理上对节点做了限制,而是如果超过了节点限制,会在zk的日志文件中记录配额超限的警告信息。
语法:setquota -n|-b val path
-n:限制子节点的数量
-b:限制节点的数据长度
val:根据-n和-b参数不同,val值的意义也不一样。如果是-n参数,val表示限制子节点的数量。如果是-b参数,val表示限制节点的数据长度
path:节点路径
10、查询节点配额
语法:listquota path
path:节点路径
Output quota:表示节点的配额信息,限制该节点最多有2个子节点,节点数据为-1,表示不限制
Output stat:表示当前节点的状态信息,该节点有4个子节点,节点数据长度为12
11、删除节点配额
语法:delquota [-n|-b] path
-n:删除子节点数量配额限制
-b:删除节点数据长度配额限制
path:节点路径
12、获取节点ACL
ACL是zk对节点权限控制的一种策略
语法:getAcl path
path:节点路径
创建节点时如果没有设置acl权限,默认为所有用户都可以对该节点进行读写操作。
13、设置节点ACL
语法:setAcl path acl
path:节点路径
acl:ACL权限模式
14、给当前客户端添加授权信息
语法:addauth scheme auth
scheme:授权方式
auth:权限
addauth一般用于digest授权方式添加授权信息。digest是用户名和密码授权,语法:username:BASE64(SHA1(password))
15、查看历史命令
可查询之前执行过的命令,会列出前最后10条命令,和linux中的history命令功能一样
history
16、执行历史命令
语法:redo cmdno
cmdno:历史命令编号
语法:sync path
path:节点路径
在对某个znode进行读操作时,应该先执行sync方法,使得读操作的连接所连的zk实例能与leader进行同步,从而保证能读到最新的数据。
注意:sync调用是异步的,无需等待调用的返回,zk服务器会保证所有后续的操作会在sync操作完成之后才执行,哪怕这些操作是在执行sync之前被提交的
18、打开或关闭监听日志
在获取节点数据、子节点列表等操作时,都可以添加watch参数监听节点的变化,从而节点数据更改、子节点列表变更时收到通知,并输出到控制台。默认是打开,可以设置参数将其关闭。
语法:printwatches on|off
on:打开
off:关闭
19、关闭连接
close命令会关闭当前客户端连接
20、连接到zk服务器
语法:connect host:port
host:port:IP和zk客户端端口
21、退出zkCli.sh终端
quit



zookeeper节点类型



持久节点(PERSISTENT)
所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。
持久顺序节点(PERSISTENT_SEQUENTIAL)
这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。
在创建节点的时候只需要传入节点 “/test_”,这样之后,zookeeper自动会给”test_”后面补充数字。
临时节点(EPHEMERAL)
和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。
这里还要注意一件事,就是当你客户端会话失效后,所产生的节点也不是一下子就消失了,也要过一段时间,大概是10秒以内,可以试一下,本机操作生成节点,在服务器端用命令来查看当前的节点数目,你会发现客户端已经stop,但是产生的节点还在。
临时顺序节点(EPHEMERAL_SEQUENTIAL)
此节点是属于临时节点,不过带有顺序,客户端会话结束节点就消失。下面是一个利用该特性的分布式锁的案例流程。
(1)客户端调用create()方法创建名为“locknode/
guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL。
(2)客户端调用getChildren(“locknode”)方法来获取所有已经创建的子节点,注意,这里不注册任何Watcher。
(3)客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点序号最小,那么就认为这个客户端获得了锁。
(4)如果在步骤3中发现自己并非所有子节点中最小的,说明自己还没有获取到锁。此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时注册事件监听。
(5)之后当这个被关注的节点被移除了,客户端会收到相应的通知。这个时候客户端需要再次调用getChildren(“locknode”)方法来获取所有已经创建的子节点,确保自己确实是最小的节点了,然后进入步骤3。



各类zookeeper java api的操作以及返回类型



创建会话



Zookeeper(String connectString,int sessionTimeout,Watcher watcher)
Zookeeper(String connectString,int sessionTimeout,Watcher watcher,boolean canBeReadOnly)
Zookeeper(String connectString,int sessionTimeout,Watcher watcher,long sessionId,byte[] sessionPasswd)
Zookeeper(String connectString,int sessionTimeout,Watcher watcher,long sessionId,byte[] sessionPasswd,boolean canBeReadOnly)



参数说明:



  1. connectString : host:port[,host:port]指定的服务器列表,多个host:port之间用英文逗号分隔。还可以可选择的指定一个基路径,如果指定了一个基路径,则所有后续操作基于这个及路径进行。例如:188.12.23.25:2181,59.23.22.25:2181 像 [ip:端口号]这样的形式

  2. sessionTimeOut – 会话超时时间。以毫秒为单位。客户端和服务器端之间的连接通过心跳包进行维系,如果心跳包超过这个指定时间则认为会话超时失效。一般设置为private static final int SESSION_TIMEOUT = 30000;

  3. watcher – 指定默认观察者。如果为null表示不需要观察者。

  4. canBeReadOnly – 是否支持只读服务。只当一个服务器失去过半连接后不能再进行写入操作时,是否继续支持读取操作。略

  5. sessionId、SessionPassword – 会话编号 会话密码,用来实现会话恢复。



返回类型:上述返回的是一个zookeeper对象 ZooKeeper zk = new ZooKeeper(hosts, zktest.SESSION_TIMEOUT, this.wh);



**注意,整个创建会话的过程是异步的,构造方法会在初始化连接后即返回,并不代表真正建立好了一个会话,此时会话处于”CONNECTING”状态。
**当会话真正创建起来后,服务器会发送事件通知给客户端,只有客户端获取到这个通知后,会话才真正建立。



创建节点



String create(final String path,byte data[],List acl,CreateMode createMode); //同步方式创建
说明:返回的是该节点的路径名称,例如:/test/zh_1324578
void create(final String path,byte data[],List acl,CreateMode createMode,StringCallback cb,Object ctx);//异步方式创建
说明:此创建方式是通过回调函数来相应操作。



参数说明:



  1. path 要创建的数据节点的路径

  2. data [] 节点创建时初始数据内容

  3. acl 节点acl安全策略

  4. createMode 创建模式
    (1)PERSISTENT 持久
    (2)PERSISTENT_SEQUENTIAL 持久顺序
    (3)EPHEMERAL 临时
    (4)EPHEMERAL_SEQUENTIAL 临时顺序

  5. cb 回调接口

  6. ctx 传递对象,用来在回调方法中使用 通常是个上下文对象



注意:不支持递归创建,即不能在无父节点的情况下创建出子节点,尝试创建已经存在的节点将失败并抛出异常,在不需要进行任何权限控制时,只需传入Ids.OPEN_ACL_UNSAFE即可。



删除节点



public void delete(final String path,int version)
说明:无返回类型
public void delete(final String path,int version,VoidCallback cb,Object ctx)
说明:无返回类型
**注意:无法删除存在子节点的节点,即如果要删除一个节点,必须要先删除其所有子节点



获取结点信息



//同步方式
List getChildren(final String path,Watcher watcher)
List getChildren(String path,boolean watch)
List getChildren(final String path,Watcher watcher,Stat stat)
List getChildren(String path,boolean watch,Stat stat)
说明:上述返回的都是path路径下的节点名称



//异步方式
void getChildred(final String path,Watcher watcher,ChildrenCallback cb,Object ctx)
void getChildred(String path,boolean watch,ChildrednCallback cb,Object ctx)
void getChildred(final String path,Watcher watcher,Children2Callback cb,Object ctx)
void getChildred(String path,boolean watch,Children2Callback cb,Object ctx)



参数说明:



  1. path 要创建的数据节点的路径

  2. watcher 观察者,一旦在本子节点获取之后,子节点列表发生变更,服务器端向客户端发送消息,触发watcher中的回调。注意,仅仅是通知而已,如果需要新的子节点列表,需要自己再次去获取。允许传入null。

  3. watch 表明是否需要注册一个Watcher。为true则通知默认到默认watcher,如果为false则不使用

  4. cb 回掉函数

  5. ctx 上下文对象

  6. stat 指定数据节点的状态信息。用法是在接口中传入一个旧的stat变量,该stat变量会在方法执行过程中,被来自服务端响应的新stat对象替换。



获取节点数据



//同步方式
byte [] getData(final String path,Watcher watcher, Stat stat)
byte [] getData(String path,boolean watch, Stat stat)
//异步方式
void getData(final String path,Watcher watcher, DataCallback cb,Object ctx)
void getData(String path,boolean watch, DataCallback cb,Object ctx)



*可以通过注册Watcher进行监听,一旦该节点数据被更新会通知客户端



更新数据



//同步方式
Stat setData(final String path,byte data[],int version)//version可以传入-1,表明要基于最新版本进行更新操作
//异步方式
void setData(final String path,byte data[],int version,StatCallback cb,Object ctx)



检查节点是否存在



//同步方式
public Stat exists(final String path,Watcher watcher)
public Stat exists(String path,boolean watch)
//异步方式
public Stat exists(final String path,Watcher watcher,StatCallback cb,Object ctx)
public Stat exists(String path,boolean watch,StatCallback cb,Object ctx)



*可以通过注册Watcher进行监听,一旦节点被创建、删除、数据被更新都会通知客户端



zookeeper权限控制



addAuthInfo(String schema,byte [] auth)
参数说明;
schema 权限模式,可以是world auth digest ip super,我们使用digest
byte[] auth 权限控制标识,由”foo:123”.getByte()方式组成,后续操作只有auth值相同才可以进行



**注意删除操作,如果在增加节点时指定了权限,则对于删除操作,认为权限加在了子节点上,删除当前结点不需要权限,删除子节点需要权限。



遇到问题:
Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
端口制定不对,配置没有生效,可以不指定server 默认连接尝试,会显示真正端口



golang链接库
https://github.com/samuel/go-zookeeper



分布式锁实现的几种方式
基于数据库实现分布式锁(表、数据库排他锁)
基于缓存(redis,memcached,tair)
基于Zookeeper实现分布式锁
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库



分布式锁算法流程如下:(优化为“等待前一个子节点删除通知”)
1.客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
2.客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;
3.执行业务代码;
4.完成业务流程后,删除对应的子节点释放锁。



Zookeeper提供一个多层级的节点命名空间(节点称为znode),每个节点都用一个以斜杠(/)分隔的路径表示,而且每个节点都有父节点(根节点除外),非常类似于文件系统。例如,/foo/doo这个表示一个znode,它的父节点为/foo,父父节点为/,而/为根节点没有父节点。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。Zookeeper为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得Zookeeper不能用于存放大量的数据,每个节点的存放数据上限为1M。



而为了保证高可用,zookeeper需要以集群形态来部署,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。客户端在使用zookeeper时,需要知道集群机器列表,通过与集群中的某一台机器建立TCP连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。



客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的zookeeper机器来处理。对于写请求,这些请求会同时发给其他zookeeper机器并且达成一致后,请求才会返回成功。因此,随着zookeeper的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。



有序性是zookeeper中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper最新的zxid。



如何使用zookeeper实现分布式锁?



在描述算法流程之前,先看下zookeeper中几个关于节点的有趣的性质:



有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号,也就是说如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。



临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。



事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更。



使用zookeeper实现分布式锁的算法流程,假设锁空间的根节点为/lock:



客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。



客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;



执行业务代码;



完成业务流程后,删除对应的子节点释放锁。



步骤1中创建的临时节点能够保证在故障的情况下锁也能被释放,考虑这么个场景:假如客户端a当前创建的子节点为序号最小的节点,获得锁之后客户端所在机器宕机了,客户端没有主动删除子节点;如果创建的是永久的节点,那么这个锁永远不会释放,导致死锁;由于创建的是临时节点,客户端宕机后,过了一定时间zookeeper没有收到客户端的心跳包判断会话失效,将临时节点删除从而释放锁。



另外细心的朋友可能会想到,在步骤2中获取子节点列表与设置监听这两步操作的原子性问题,考虑这么个场景:客户端a对应子节点为/lock/lock-0000000000,客户端b对应子节点为/lock/lock-0000000001,客户端b获取子节点列表时发现自己不是序号最小的,但是在设置监听器前客户端a完成业务流程删除了子节点/lock/lock-0000000000,客户端b设置的监听器岂不是丢失了这个事件从而导致永远等待了?这个问题不存在的。因为zookeeper提供的API中设置监听器的操作与读操作是原子执行的,也就是说在读子节点列表时同时设置监听器,保证不会丢失事件。



最后,对于这个算法有个极大的优化点:假如当前有1000个节点在等待锁,如果获得锁的客户端释放锁时,这1000个客户端都会被唤醒,这种情况称为“羊群效应”;在这种羊群效应中,zookeeper需要通知1000个客户端,这会阻塞其他的操作,最好的情况应该只唤醒新的最小节点对应的客户端。应该怎么做呢?在设置事件监听时,每个客户端应该对刚好在它之前的子节点设置事件监听,例如子节点列表为/lock/lock-0000000000、/lock/lock-0000000001、/lock/lock-0000000002,序号为1的客户端监听序号为0的子节点删除消息,序号为2的监听序号为1的子节点删除消息。



调整后的分布式锁算法流程如下:



客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推;



客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;



执行业务代码;
完成业务流程后,删除对应的子节点释放锁。



虽然zookeeper原生客户端暴露的API已经非常简洁了,但是实现一个分布式锁还是比较麻烦的…我们可以直接使用curator这个开源项目提供的zookeeper分布式锁实现。



1、互斥锁mutex lock
顾名思义就是排它锁,同一时间只允许一个客户端执行。



实现步骤:



首先,创建一个lock node,例如“locknode”
其次,客户端lock执行以下方式:
创建(create)一个有序临时节点,例如“locknode/guid-lock-”,其中guid可以是你客户端的唯一识别序号,如果发生前面说的创建失败问题,需要使用guid进行手动检查。
调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知。
从这个列表中,判断自己创建的节点序号是否是最小,如果是则直接返回true,否则继续往下走。
从步骤2中获取的list中选取排在当前节点前一位的节点,调用exist(watch=true)方法。
如果exist返回false,则回到步骤2;
如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2.
最后,客户端unlock只需要调用delete删除掉节点即可。


优点:



避免了轮询和超时控制
每次一个子节点的删除动作,只会触发唯一一个客户端的watch动作,从而避免了羊群效应
便于观测



缺点:



没有解决锁重入问题,因为采用的是有序临时节点,因此多次调用create并不会触发KeeperException.NodeExists异常,从而无法实现锁重入功能。如果需要解决,则在步骤1时,需要先进行判断当前节点是否已经存在,即调用getChildren(watch=false),判断当前节点是否已经创建(配合guid),已经创建,则直接从步骤3开始,没有创建则从步骤1开始。
这是一个公平锁,无法实现非公平锁。参考[4]实现了一个非公平锁



注意:



如果一个节点创建了一个sequential ephemeral nodes,但是在server返回创建成功的时候,server挂了,此时客户端需要重新连接,重新连接后会话依然有效,但其创建的临时节点却没有删除。解决方式就是在每次创建时create,如果发生失败,客户端需要getChildren(),进行手动检查是否获取锁,这个时候就需要使用guid。



2、共享锁Shared Locks或读写锁Read/Write Locks



Read读锁是共享锁,Write写锁是排它锁,当没有写时,允许多个read实例获取读锁,当有一个write实例获得写锁时,则不允许任何其他write实例和read实例获得锁。



实现步骤:



首先,创建一个lock node,例如“locknode”
获取read锁步骤:
创建(create)一个有序临时节点,例如“locknode/read-guid-lock-”,其中guid可以是你客户端的唯一识别序号,如果发生前面说的创建失败问题,需要使用guid进行手动检查。
调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知。
从这个列表中,判断是否有序号比自己小、且路径名以“write-”开头的节点,如果没有,则直接获取读锁,否则继续如下步骤。
从步骤2中获取的list中选取排在当前节点前一位的、且路径名以“write-”开头的节点,调用exist(watch=true)方法。
如果exist返回false,则回到步骤2。
如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2。
获取write锁步骤:
创建(create)一个有序临时节点,例如“locknode/write-guid-lock-”,其中guid可以是你客户端的唯一识别序号,如果发生前面说的创建失败问题,需要使用guid进行手动检查。
调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知。
从这个列表中,判断自己创建的节点序号是否是最小,如果是则直接返回true,否则继续往下走。
从步骤2中获取的list中选取排在当前节点前一位的节点,调用exist(watch=true)方法。
如果exist返回false,则回到步骤2;
如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2.
最后,客户端unlock只需要调用delete删除掉节点即可。



优点:



避免了轮询和超时控制
每次一个子节点的删除动作,只会触发唯一一个客户端的watch动作,从而避免了羊群效应
便于观测



缺点:



没有解决锁重入问题,因为采用的是有序临时节点,因此多次调用create并不会触发KeeperException.NodeExists异常,从而无法实现锁重入功能。如果需要解决,则在步骤1时,需要先进行判断当前节点是否已经存在,即调用getChildren(watch=false),判断当前节点是否已经创建(配合guid),已经创建,则直接从步骤3开始,没有创建则从步骤1开始。
当有非常多的read节点在等待一个write节点删除通知时,一旦write节点删除,将会触发非常多的read节点被调用,不过这种情况无法避免。



可撤销和超时问题



当前的读写锁并没有考虑读锁可撤销和超时问题,如何让读锁主动放弃,如何判断超时等,我想可行的方案还是在客户端自己处理,如果其他客户端想让前面的节点放弃锁,可以在节点写入unlock信息,让持有锁的客户端监听该变化,收到unlock信息,自己主动放弃对锁的持有。



Category zookeeper