背景:
分布式锁可以很简单的使用Redis的setnx实现,另外也可以基于Zookeeper来做。
Zookeeper官方给出了几个使用场景:Barriers,Queues,Locks,Two-phased Commit,Leader Election。
其中对Locks的描述是“Fully distributed locks that are globally synchronous, meaning at any snapshot in time no two clients think they hold the same lock. These can be implemented using ZooKeeeper.”(完全分布式的全局同步锁,意思是在任何同一时间下,不会有两个客户端拥有同一个锁。这可以用zookeeper实现。)
原理:
Zookeeper没有在其Java Api里直接提供这样一个锁,而是给出了一份recipes(中文意思是“菜谱”,应该是实现指南之类的意思吧),地址:http://zookeeper.apache.org/doc/r3.4.6/recipes.html
我们主要关心其中的写入锁的实现。
大致的思想是,在zk的一个PERSISTENT父节点下,每个客户端(请求)创建一个EPHEMERAL_SEQUENTIAL节点,这种节点的特性是临时的,在zookeeper会话断线后会自动删除,并且是有序列顺序的,比如客户端a请求创建节点前缀为“prefix-sessionId-”,则zk生成的节点名类似“prefix-sessionId-0000000001”,客户端b再用同样的前缀请求创建节点,zk会生成“prefix-sessionId-0000000002”,zk保证了EPHEMERAL_SEQUENTIAL节点的这种特性。
获得锁:
每个客户端创建了节点后,紧接着去查询父节点(PERSISTENT)下的节点(EPHEMERAL_SEQUENTIAL),zk返回一个节点的列表,客户端将列表进行一个按序列从小到大的排序,并观察自己的节点是否是列表第一个,若是,则说明获得了锁。若不是,则对自己节点的前一个节点设置watcher,等待它释放锁后重新尝试获得锁。
释放锁:
客户端将自己的节点删除,这会触发对本客户端节点设置watcher的节点(如果有)的注册事件得到触发。
实现:
根据官方“菜谱”,获得锁的实现主要分为5步:
1. 调用create()创建路径名为“locknode/write-”的节点。确保设置了sequence和ephemeral标志。
2. 再锁的父节点上调用getChildren(),不能设置watch,这很关键,因为这样避免的羊群效应。
3. 如果没有节点的序列比步骤1创建的节点小,客户端将获得锁然后退出程序。
4. 在比步骤1创建的节点序列小的前一个节点上,调用exists()并设置watch。
5. 如果exist()返回false,回到步骤2。否则等待第4步的通知,然后回到步骤2。
实际上,官方在zookeeper源码的“菜谱”目录下,已经给出了一个实现,地址:
https://svn.apache.org/repos/asf/zookeeper/trunk/src/recipes/lock
但是,在我的工作场景中,我能够合法使用的zookeeper父节点下已经存在了一些永久节点,他们都是Dubbo接口名。首先,官方实现每次调用都会ensurePathExists,即确保父节点存在,这在我们的应用场景是没有必要的。
其次,官方实现里面直接对父节点getChildren,从而获得了所有子节点,而我们的不需要得到那些dubbo接口名,这可以通过设置一个前缀(包含Java方法名不允许出现的符号,比如‘-’)来实现。
然后,官方实现第4步,代码如下:
Stat stat = zookeeper.exists(lastChildId, new LockWatcher());
if (stat != null) {
return Boolean.FALSE;
} else {
LOG.warn("Could not find the stats for less than me: ", lastChildName.getName());
}
Watcher代码:
private class LockWatcher implements Watcher {
public void process(WatchedEvent event) {
// lets either become the leader or watch the new/updated node
LOG.debug("Watcher fired on path: " + event.getPath() + " state: " +
event.getState() + " type " + event.getType());
try {
lock();
} catch (Exception e) {
LOG.warn("Failed to acquire lock: " + e, e);
}
}
}
在exist设置watcher后,直接返回了false,表式本次获取锁失败,但是在watcher中继续取尝试获得锁,相当于用另一个线程继续重试。
另外,官方锁实现并不包含zk客户端的实现。在简单的分布式锁实现的情景下,可以自己实现,也有zkClient。
异常处理:
官方文档:http://wiki.apache.org/hadoop/ZooKeeper/ErrorHandling
主要需要注意两种异常:
1.KeeperException.ConnectionLossException(client与其中的一台server socket链接出现异常)
2.KeeperException.SessionExpiredException(client的session超过sessionTimeout)
第一种,直接重试,zk会自动尝试重连。
第二种,需要重新实例化zk客户端,锁也需要重新获取。
实现:https://github.com/fankux/ZkLocker
另外,欢迎随意拍砖。