风一样的男人
作者风一样的男人2017-09-29 11:22
系统工程师, 长春一汽

基于Zk实现分布式锁

字数 3180阅读 1900评论 0赞 3

最近项目涉及系统集群运行时,由于交叉配置,可能引起多台服务器运行时处理同一份数据,所以要用分布式锁控制。

下面就是基于Zk实现的一个分布式锁。

Zookeeper overview

Zookeeper给其client呈现的是按层次组织的节点(znode),组织方式与文件系统类型,如下图,每个znode中可以包含一些数据。Zookeeper中有两种类型的znode,Regular和Ephemeral。对于Regular znode,其由client显式的创建和删除;对于Ephemeral znode,其由client创建,可由client显式删除,或当创建该Ephemeral znode的session终止时由zookeeper自动删除。

关于zookeeper中的session,当client连接zookeeper时,会初始化一个session,session有一个超时时间,如在超时时间内,zookeeper没有从client收到任何信息(zookeeper会发状态检测信息),则认为client故障了,此时zookeeper会关闭这个session(session也可由client显式关闭)。

另外,client在创建znode时还可以指定一个sequential flag,创建的znode将会拥有一个递增的序号,该序号会加到znode的名字后面。

微信图片_20170929111716.jpg

微信图片_20170929111716.jpg

Zookeeper API

构造一个zookeeper client实例,需要指定一个服务器列表(以逗号分割的ip:port列表),并指定session超时时间及默认的watch回调接口。

create(path, data, flags) :创建一个由path指定的znode,该znode中存储data,flag用于设置znode的类型(regular、ephemeral、sequential..),返回新建znode的名字。虽然znode具有存储功能,但强烈不建议将其用于存储大量数据,通常znode中存储一些metadata。

delete(path, version) : 如果znode的version与参数中version相同,删除由path指定的znode。

exists(path, watch) : znode是否存在,如果设置watch标记,则返回的信息变化时会通知client(不存在—>存在,存在—>不存在两种变化都会通知)。

getData(path, watch) : 获取znode的data,可设置watch标志(当该zonde的data变化时得到通知),如果znode不存在watch不会被设置。

setData(path, data, version) : 设置znode的data,与delete相同需要检查version信息。

getChildren(path, watch) : 获取子节点的名字列表,可设置watch(当创建或删除子节点时会得到通知)。

sync(path) : 等待所有的pending update被执行。

Zookeeper的API接口拥有同步和异步两个版本,使用异步API时,client可为每个operation设置callback,在operation被执行后,zookeeper会执行对应的callback。

Zookeeper所有的update接口都带version信息(-1表示不检查version信息),用于实现condition update,在update前检查version是否匹配,只有在version信息匹配时,update才会被更新,有点类似与CAS。

1. 思路

解决方案依然很简单,需要加锁的进程先尝试在zookeeper上创建一个临时节点L,如果创建成功则加锁成功,如果不成功(已存在)则在该节点上设置watch。进程通过删除L来解锁(当进程意外终止,L也会被删除,不会造成死锁),当L被删除时,其它等待锁的进程会得到通知,此时这些进程再次创建L来获得锁。

上面的方案,当竞争锁的进程比较多时,解锁时会引起Herd Effect,可对加锁规则进行限制,如按进程尝试加锁的顺序来分配锁。在zookeeper上,每个加锁的进程创建一个带SEQUENTIAL标志的临时节点,每次让序号最小的节点获得锁,这样每个节点只需要watch它前面节点的状态即可,当其前面节点被删除时,其将被通知,并获得锁。改进如下:

微信图片_20170929111803.jpg

微信图片_20170929111803.jpg

如果要实现读写锁,则要做进一步改进,要获得写锁的进程按上述方式竞争锁,要获得读锁的进程则watch序号比自己小的写进程,改进如下:

微信图片_20170929111823.jpg

微信图片_20170929111823.jpg

ZkClient Overview

1.提供了Zk断链重连的特性::这个特性似乎每个开发者都会设计,而且代码风格几乎"如出一辙"。在大部分Zk使用场景中,我们都要求它能够在断链的时候,重新建立连接,无论session失效与否。
2.Event监听器机制:向ZNode节点注册watch,每个开发者都使用过,尽管watch机制并不能确保数据变更的实时性。Watch-Event属于"即发即失",因为我们需要得到Event时候,再去注册一遍,这也是一个非常繁琐的事情,I0Itec-ZkClient提供了Event-Listener的小技巧,可以帮助我们"解脱"。
3.Zk异常处理:Zkr中繁多的Exception,以及每个Exception所需要关注的事情各有不同,你应该记得那一堆try-catch给你带来的烦恼。
4.Data序列化:简单的data序列化(Serialzer/Deserialzer)。

ZkClient API

1.ZkConnection类:对Zk API的简单分装,提供了链接Zk Server和数据CRUD的操作;此类实现了IZkConnection接口,通常情况下,如果I0Itec-Zkclient不能满足需要的时候,我们可以重写ZkConnection即可。
2.ZkClient类:核心类,也是开发者需要直接使用的类,它内部维护了Zk的链接管理和Event处理逻辑等,同时也暴露了Zookeeper Znode的CRUD方法列表。
3.IZkChildListener接口:ZNode 子节点事件侦听器,当ZkClient接收到某个path节点变更或者子节点变更事件时,会触发Listener。
4.IZkDataListener接口
5.IZkStateListener接口:当Zk客户端状态变更时,触发。

2. 实现

private ZkClient zkClient;

private String path;
private final String LOCK;

public boolean lock() throws Exception {
    if (zkClient.exists(path))
        return true;
    else
        return zkClient.create(path, LOCK.getBytes(), CreateMode.EPHEMERAL) == null ? true : false;
}

public boolean unlock() throws Exception {
    return zkClient.delete(path);
}

public boolean islock() throws Exception {
    return zkClient.exists(path);
}

本文转自微信公众号:互联网后端架构

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

3

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关问题

相关资料

X社区推广