基於臨時序號節點來實現分散式鎖
為什麼要用臨時節點呢?如果拿到鎖的服務當機了,會話失效ZK自己也會刪除掉臨時的序號節點,這樣也不會阻塞其他服務。
流程:
1.在一個持久節點下面建立臨時的序號節點作為鎖節點,如:/lock/lockId00000001 /lock/lockId00000002
2.獲取持久節點下面所有子節點,判斷其最小的是不是自己,如果是自己則表示獲取鎖成功。
3.如果最小的結點不是自己,則阻塞等待,並對lock結點新增監聽,如果結點數發生變化了,則說明有釋放了鎖。但是這裡如果存在大量監聽,當結點發生變化的時候,可能就會出現羊群效應。因為大家都監聽了,同時會通知很多客戶端,會造成ZK效能突然下降。
所以這裡可以只監聽自己前面的那個節點,排隊執行,03監聽02,02監聽01
4.當鎖釋放,節點監聽到變化之後,執行第2步。
釋放鎖只需要刪除對應的臨時節點,如果獲取到鎖的服務當機了,因為是臨時節點也會自己刪除的。
程式碼實現:
package com.nijunyang.zookeeper.zklock; import lombok.Data; import org.I0Itec.zkclient.IZkDataListener; import org.I0Itec.zkclient.ZkClient; import java.util.List; import java.util.stream.Collectors; /** * Description: * Created by nijunyang on 2020/11/29 21:51 */ public class ZKLock { private static final String SERVER = "192.168.0.67:2181"; private ZkClient zkClient; private static final String ROOT_PATH = "/lock"; public ZKLock() { zkClient = new ZkClient(SERVER, 5000, 20000); buildRoot(); } private void buildRoot() { if (!zkClient.exists(ROOT_PATH)) { zkClient.createPersistent(ROOT_PATH); } } /** * 加鎖 * * @param lockId * @param timeout * @return */ public Lock lock(String lockId, long timeout) { Lock lockNode = createLock(lockId); // 嘗試啟用鎖 tryActiveLock(lockNode); if (!lockNode.isActive()) { try { synchronized (lockNode) { lockNode.wait(timeout); } } catch (InterruptedException e) { throw new RuntimeException(e); } } if (!lockNode.isActive()) { throw new RuntimeException("獲取鎖超時"); } return lockNode; } /** * 釋放鎖 * * @param lock */ public void unlock(Lock lock) { if (lock.isActive()) { zkClient.delete(lock.getPath()); } } /** * 啟用鎖 * @param lockNode */ private void tryActiveLock(Lock lockNode) { // 判斷當前是否為最小節點 List<String> list = zkClient.getChildren(ROOT_PATH) .stream() .sorted() .map(p -> ROOT_PATH + "/" + p) .collect(Collectors.toList()); String firstNodePath = list.get(0); //第一個節點是當前節點,將鎖設定為啟用 if (firstNodePath.equals(lockNode.getPath())) { lockNode.setActive(true); } else { //第一個節點不是當前節點,則監聽前面一個節點 String upNodePath = list.get(list.indexOf(lockNode.getPath()) - 1); zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() { @Override public void handleDataChange(String dataPath, Object data) throws Exception { } @Override public void handleDataDeleted(String dataPath) throws Exception { //監聽之後繼續嘗試加鎖,加鎖成功喚醒之前的等待 tryActiveLock(lockNode); synchronized (lockNode) { if (lockNode.isActive()) { lockNode.notify(); } } zkClient.unsubscribeDataChanges(upNodePath, this); } }); } } /** * 建立鎖節點 * * @param lockId * @return Lock */ private Lock createLock(String lockId) { String nodePath = zkClient.createEphemeralSequential(ROOT_PATH + "/" + lockId, "write"); return new Lock(lockId, nodePath); } @Data public static class Lock { private String lockId; private String path; /** * 是否啟用鎖 */ private boolean active; public Lock(String lockId, String path) { this.lockId = lockId; this.path = path; } } }