本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
在《Redis怎样实现分布式锁?》和《Redis怎样实现可重入分布式锁?》中,我们了解到了为什么需要分布式锁,并且学会了怎样用Redis实现分布式锁。不过分布式锁除了可以使用Redis实现以外,还可以使用数据库或者Zookeeper实现,本文将为大家讲解怎样用Zookeeper实现分布式锁。
Zookeeper节点类型
在Zookeeper中,节点分为多种类型,具体如下:
- 持久节点
节点创建后,不管是否跟客户端断开连接,节点都会一直存在。 - 临时节点
节点创建后,只要与客户端断开连接,节点就会消失。 - 顺序节点
节点创建后,Zookeeper会根据创建的时间顺序,自动在节点名称后面增加上类似0000001的数字表示顺序。
顺序节点既可以是持久节点,也可以是临时节点。
Zookeeper监听机制
Zookeeper提供监听机制,用来支持客户端进行相应状态或者事件监听,目前支持的事件或者状态监听如下:
- 通知状态
- SyncConnected:客户端与服务器正常连接时通知
- Disconnected:客户端与服务器断开连接时通知
- Expired:会话Session过期时通知
- AuthFailed:身份认证失败时通知
- 事件类型(基于SyncConnected状态)
- None:无
- NodeCreated:监听的节点被创建时通知
- NodeDeleted:监听的节点被删除时通知
- NodeDataChanged:监听的节点数据变更时通知
- NodeChildrenChanged:监听的节点的子节点列表发生变更时通知
Zookeeper分布式锁实现逻辑
Zookeeper分布式锁的实现就是基于临时顺序节点和节点删除通知来实现的。
加锁
加锁时,我们首先根据锁标识,在指定的锁根目录下创建一个持久化节点,即为锁节点,代码如下:
// 锁节点路径
String lockParentPath = LOCK_ROOT_PATH+"/"+lockKey;
try {
// 创建锁节点
zooKeeper.create(lockParentPath,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
} catch (Exception e) {
logger.error("创建锁节点异常,lockParentPath:{}", lockParentPath, e);
}
接下来,我们通过在创建好的锁节点下面创建临时顺序子节点,如果当前线程创建好的临时顺序子节点的序号是最小的,则表示获取到锁。代码如下:
this.lockPath = zooKeeper.create(lockParentPath+"lock_", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 校验是否是最小节点,是则获取成功,不是则监听前一个节点
if (getLockAndWatchLast()) {
logger.info("加锁成功,lockKey:{}", lockKey);
}
若当前线程获取到的临时顺序子节点的序号不是最小的,则需要监听其前一个节点,在前一个节点被删除时,重新校验当前线程的临时顺序子节点的序号是不是最小的,如果是,则获取锁成功。代码如下:
CountDownLatch latch = new CountDownLatch(1);
// 获取前一个节点
String prePath = getPrePath(children, curNodeName);
// 监听prePath是否存在
zk.exists(prePath, new Watcher() {
public void process(WatchedEvent event) {
// prePath不存在
latch.countDown();
}
});
// 闭锁等待
latch.await();
// 重新校验并获取锁
getLockAndWatchLast();
释放锁
删除当前线程获取到的锁临时节点即可。如果当前锁已经没有子节点,则表示当前没有其它线程竞争锁,则把锁对应的路径也进行删除。
Zookeeper分布式锁完整代码
public class ZookeeperDistributedLock implements Lock {
/**线程本地存储,存储锁的密钥和计数:结构为 锁id:加锁计数*/
private static ThreadLocal<Map<String, Integer>> threadLocal = new ThreadLocal<>();
// 锁根目录
private String LOCK_ROOT_PATH="/test/locks";
// 锁键值
private String lockKey;
// Zookeeper客户端
private ZooKeeper zooKeeper;
// 当前线程获取到的锁路径
private String lockPath;
public ZookeeperDistributedLock(String lockKey, ZooKeeper zooKeeper) {
this.lockKey = lockKey;
this.zooKeeper = zooKeeper;
}
/**
* 尝试获取锁
*/
@Override
public boolean tryLock() {
// 当前线程持有的锁及其计数
Map<String,Integer> value = threadLocal.get();
if(value.containsKey(lockKey)){
value.put(lockKey,value.get(lockKey) + 1);
return true;
}
// 获取锁成功
if (getLock()) {
value.put(lockKey,1);
return true;
}
return false;
}
/**
* 获取锁
*/
private boolean getLock() {
// 锁节点路径
String lockParentPath = LOCK_ROOT_PATH+"/"+lockKey;
try {
// 创建锁节点
zooKeeper.create(lockParentPath,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 执行加锁
return doLock(lockParentPath);
} catch (Exception e) {
logger.error("创建锁节点异常,lockParentPath:{}", lockParentPath, e);
}
return false;
}
// 执行加锁
private boolean doLock(String lockParentPath) {
try {
this.lockPath = zooKeeper.create(lockParentPath+"lock_", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 校验是否是最小节点,是则获取成功,不是则监听前一个节点
if (getLockAndWatchLast()) {
logger.info("加锁成功,lockKey:{}", lockKey);
}
} catch(Exception e) {
logger.error("执行加锁异常,lockParentPath:{}", lockParentPath, e);
}
return false;
}
// 校验是否是最小节点,是则获取成功,不是则监听前一个节点
private boolean getLockAndWatchLast() {
List<String> children = zooKeeper.getChildren(lockParentPath, false);
Collections.sort(children);
String[] paths=this.lockPath.split("/");
String curNodeName = paths[paths.length-1];
if (children.get(0).equalsIgnoreCase(curNodeName)) {
return true;
}
CountDownLatch latch = new CountDownLatch(1);
// 获取前一个节点
String prePath = getPrePath(children, curNodeName);
// 监听prePath是否存在
zk.exists(prePath, new Watcher() {
public void process(WatchedEvent event) {
// prePath不存在
latch.countDown();
}
});
// 闭锁等待
latch.await();
// 重新校验并获取锁
getLockAndWatchLast();
}
// 获取前一个节点
private String getPrePath(List<String> children, String curNodeName) {
for (int i = 0; i < children.size(); i++) {
String child = children.get(i);
if (child.equalsIgnoreCase(curNodeName)) {
break;
}
}
return children.get(i - 1);
}
/**
* 释放锁
*/
@Override
public void unlock(String lockKey,String requestId) {
// 当前线程持有的锁及其计数
Map<String,Integer> value = threadLocal.get();
if(value.containsKey(lockKey)){
Integer num = value.get(lockKey) - 1;
// 锁计数小于等于0,需要调用Zookeeper进行锁释放
if(num <= 0){
// 释放锁
if (doUnLock()) {
logger.info("释放锁成功,lockKey:{}", lockKey);
value.remove(lockKey);
} else {// 失败
logger.info("释放锁失败,lockKey:{}", lockKey);
}
} else {
// 减少锁计数
value.put(lockKey,num);
}
} else {
logger.info("释放锁成功,当前线程未持有锁,lockKey:{}", lockKey);
}
}
// 释放锁
private boolean doUnLock() {
try {
if (this.lockPath != null) {
// 删除节点,释放锁
zooKeeper.delete(this.lockPath, -1);
}
// 锁节点路径
String lockParentPath = LOCK_ROOT_PATH+"/"+lockKey;
List<String> children = zooKeeper.getChildren(lockParentPath, false);
if (children.isEmpty()) {
try {
// 删除锁节点
zooKeeper.delete(lockParentPath, -1);
} catch (KeeperException e) {
logger.error("删除锁节点异常,lockParentPath:{}", lockParentPath, e);
}
}
// 关闭Zookeeper连接
if (zooKeeper != null) {
zooKeeper.close();
}
} catch(Exception e) {
logger.error("释放锁异常,lockParentPath:{}", lockParentPath, e);
}
return false;
}
}
后言
既然看到这里了,感觉有所收获的朋友,不妨来个大大的点赞吧~~~