ZooKeeper-3.5.6分佈式鎖

原理
基本方案是基於ZooKeeper的臨時節點與和watch機制。當要獲取鎖時在某個目錄下創建一個臨時節點,創建成功則表示獲取鎖成功,創建失敗則表示獲取鎖失敗,此時watch該臨時節點,當該臨時節點被刪除後再去嘗試獲取鎖。臨時節點好處在於,當客戶端崩潰後自動刪除臨時節點的同時鎖也被釋放了。該方案有個缺點缺點,就是大量客戶端監聽同一個節點,當鎖釋放後所有等待的客戶端同時嘗試獲取鎖,併發量很大。因此有了以下優化的方案。
優化方案基於ZooKeeper的臨時有序節點和watch機制。當要獲取鎖時在某個目錄下創建一個臨時有序節點,每次創建均能成功,只是返回的節點序號不同。只有返回的節點序號是該目錄下最小的才表示獲取鎖成功,否則表示獲取鎖失敗,此時watch節點序號比本身小的前一個節點,當watch的節點被刪除後再去嘗試獲取鎖。
 
示例
1.使用Maven引入ZooKeeper客戶端:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.6</version>
</dependency>
2.工具類
package com.example.demo;
 
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
 
public class ZkLockUtils implements Watcher {
 
private ZooKeeper zk;
 
/**
* 鎖根節點
*/
private String lockRoot;
 
/**
* 鎖子節點前綴
*/
private String lock;
 
/**
* 鎖子節點分隔字符串
* 用於篩選鎖根節點下和鎖相關的子節點
*/
private String splitStr;
 
/**
* 要監聽的鎖子節點
*/
private String watchNode;
 
/**
* 當前創建的鎖子節點
*/
private String currentNode;
 
/**
* 等待計數器
*/
private CountDownLatch latch;
 
/**
* 初始化
*
* @param connectString 連接字符串,如果多個則使用逗號分隔,例如127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002
* @param lockRoot 鎖根節點,“/”開頭
* @param lock 鎖子節點前綴,不帶“/”
* @param splitStr 鎖子節點分隔字符串
* @throws Exception Exception
*/
public ZkLockUtils(String connectString, String lockRoot, String lock, String splitStr) throws Exception {
this.lockRoot = lockRoot;
this.lock = lock;
if (lock.contains(splitStr)) {
throw new RuntimeException("lock不能包含splitStr");
}
this.splitStr = splitStr;
 
// 會話超時時間30秒
int sessionTimeout = 30000;
// 初始化zk客戶端,監視器設置爲當前類
zk = new ZooKeeper(connectString, sessionTimeout, this);
// 如果鎖根節點不存在,則創建
Stat stat = zk.exists(lockRoot, false);
if (stat == null) {
zk.create(lockRoot, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
 
/**
* 收到通知時執行
*/
public void process(WatchedEvent event) {
if (latch != null) {
latch.countDown();
}
}
 
/**
* 獲取鎖,如果超過等待時間,則獲取鎖失敗
*
* @param waitTime 等待時間
* @param timeUnit 時間單位
* @return true(成功) false(失敗)
* @throws Exception Exception
*/
public boolean lock(long waitTime, TimeUnit timeUnit) throws Exception {
// 創建臨時有序鎖子節點
currentNode = zk.create(lockRoot + "/" + lock + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 取出所有鎖根節點的子節點
List<String> childNodes = zk.getChildren(lockRoot, false);
// 篩選鎖根節點下和鎖相關的子節點
List<String> lockChildNodes = new ArrayList<>();
for (String childNode : childNodes) {
String str = childNode.split(splitStr)[0];
if (str.equals(lock)) {
lockChildNodes.add(childNode);
}
}
// 升序排序
Collections.sort(lockChildNodes);
// 如果是最小的鎖子節點,則獲得鎖
if (currentNode.equals(lockRoot + "/" + lockChildNodes.get(0))) {
System.out.println("當前是最小的鎖子節點,獲取鎖成功");
return true;
}
// 如果不是最小的鎖子節點,找到索引比自己小1的鎖子節點
String previousNode = currentNode.substring(currentNode.lastIndexOf("/") + 1);
watchNode = lockChildNodes.get(Collections.binarySearch(lockChildNodes, previousNode) - 1);
 
// 判斷要監聽的鎖子節點是否存在並監聽該節點,如果不存在則該節點已被刪除,直接獲得鎖
Stat stat = zk.exists(lockRoot + "/" + watchNode, true);
if (stat != null) {
latch = new CountDownLatch(1);
boolean b = latch.await(waitTime, timeUnit);
latch = null;
if (b) {
System.out.println("監聽到鎖子節點被刪除,獲取鎖成功");
return true;
} else {
System.out.println("等待超時,獲取鎖失敗");
return false;
}
} else {
System.out.println("要監聽的鎖子節點不存在,獲取鎖成功");
return true;
}
}
 
/**
* 釋放鎖
*/
public void unlock() throws Exception {
// version爲-1表示匹配任何版本
zk.delete(currentNode, -1);
watchNode = null;
currentNode = null;
zk.close();
}
}
3.測試
package com.example.demo;
 
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
 
public class Demo {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(2);
 
Thread thread1 = new Thread(() -> {
ZkLockUtils zkLockUtils = null;
try {
zkLockUtils = new ZkLockUtils("127.0.0.1:3181,127.0.0.1:3182,127.0.0.1:3183", "/locks", "lock", "_");
zkLockUtils.lock(5, TimeUnit.SECONDS);
Thread.sleep(10000);
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (zkLockUtils != null) {
try {
zkLockUtils.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
 
thread1.start();
 
Thread thread2 = new Thread(() -> {
ZkLockUtils zkLockUtils = null;
try {
zkLockUtils = new ZkLockUtils("127.0.0.1:3181,127.0.0.1:3182,127.0.0.1:3183", "/locks", "lock", "_");
zkLockUtils.lock(5, TimeUnit.SECONDS);
Thread.sleep(10000);
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (zkLockUtils != null) {
try {
zkLockUtils.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
 
thread2.start();
 
countDownLatch.await();
System.out.println("所有子線程都執行完畢");
}
}
 
優缺點
1.Redis分佈式鎖需要輪詢獲取鎖,性能開銷較大。ZooKeeper分佈式鎖基於watch機制監聽節點,不需要輪詢獲取鎖,性能開銷較小。
2.如果Redis獲取鎖的客戶端非正常退出,那麼只能等待超時時間之後才能釋放鎖。Redis因爲創建的是臨時節點,只要客戶端崩潰或者連接斷開,臨時節點就會被刪除,鎖也就被立即釋放了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章