利用zookeeper手動實現分佈式鎖

前言

分佈式鎖,是控制分佈式系統之間同步訪問共享資源的一種方式。在分佈式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分佈式鎖。

對於分佈式鎖,想必大家都不陌生。分佈式鎖的作用和Synchronized的作用一樣,都是限制共享資源的訪問。只不過Synchronized只能作用於單機,而分佈式鎖作用於分佈式系統。

想要實現一個簡單的鎖,其實只需實現兩個部分:隊列同步器。同步器用來控制任務(task)和線程的綁定與解綁,隊列用來存放暫未搶到鎖的線程。簡單來說,就是怎麼處理搶到鎖的線程,和怎麼處理沒搶到鎖的線程。這不僅僅適用於單機鎖,也同樣適用於分佈式鎖。當然,如果想要實現可重入可中斷公平非公平以及類似wait/notify等功能,那就相對複雜了。

前置閱讀

這兩篇分別介紹了zookeeper集羣的搭建,以及zookeeper響應式API介紹,需要的自取。

zookeeper

爲什麼zookeeper適合做分佈式鎖?

  • zookeeper的Znode節點類似Linux系統的文件目錄,且同一目錄下Znode名稱唯一,這個唯一性就相當於天然支持了鎖
  • zookeeper支持臨時節點,session斷開後會自動刪除臨時節點,所以即使加鎖的線程中途異常,也不會發生死鎖
  • zookeeper支持watch機制,能及時通知各個事件
  • zookeeper自帶高可用,穩定壓倒一切
  • zookeeper性能強悍(相比之下)

想要利用zookeeper實現一個分佈式鎖,首先要解決哪些問題呢?

  • 多個線程爭搶鎖的時候,只有一個線程能搶到鎖
  • 沒有搶到鎖的線程需要排成一個隊列
  • 搶到鎖的線程執行完釋放鎖之後,需要通知其他線程來搶鎖

對於以上三個問題,整體思路是在同一個目錄下每個線程創建一個臨時序列節點,這樣每個線程創建的節點名稱就會是xxxx0000000001xxxx0000000002xxxx0000000003這種形式。我們假定節點序號最小的那個節點就是鎖,也就是說哪個線程創建的節點序號最小,就認爲哪個線程搶到了鎖,其餘的線程暫時只能等着。等待搶到鎖的線程執行完成後刪除自己創建的節點。再通過watch機制來通知其他線程爭搶鎖。

這樣相當於實現了鎖的兩個部分:隊列同步器。按序號排列的節點相當於處在同一個隊列中。線程通過創建、刪除節點來表示獲取、釋放鎖,相當於一個同步器。

除此之外,還有一點需要被關注。搶到鎖的線程執行完成,釋放鎖(刪除Znode)之後,是否需要通知所有的節點。如果是的話,那麼每次釋放鎖之後,所有的線程都來競爭鎖,但是隻會有一個線程搶到鎖,其餘的線程又得被阻塞,這樣勢必會導致大量的無意義的上下文切換。這類問題有一個專業術語叫驚羣效應

爲了避免這個問題,每次鎖被釋放後,應該只通知下一個獲取鎖的線程。也就是自己被刪除後,下一個創建了最小節點的線程。這就要求每個節點在註冊watch事件時,不能watch整個鎖目錄,只能watch自己的前一個節點。

整個鎖的示意圖如下
zookeeper分佈式鎖
每個Znode只watch自己的前一個節點,這樣鎖釋放後只需要通知一個節點即可。

源代碼

獲取Zookeeper對象

package com.sicimike.zk.lock;

import com.google.common.base.Joiner;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

/**
 * @author sicimike
 * @create 2020-05-03 21:15
 */
public class ZookeeperUtil {

	// /zkLock表示指定鎖的根目錄,後續獲取"/"都是這個目錄
    private static final String[] ZK_SERVER = {
            "192.168.1.101:2181", "192.168.1.102:2181",
            "192.168.1.103:2181", "192.168.1.104:2181/zkLock",
    };
    private static final int SESSION_TIMEOUT = 10000;
    private static final CountDownLatch latch = new CountDownLatch(1);


    public static ZooKeeper newInstance() throws IOException, InterruptedException {
        ZooKeeper zooKeeper = new ZooKeeper(Joiner.on(",").join(ZK_SERVER), SESSION_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                switch (event.getState()) {
                    case SyncConnected:
                        latch.countDown();
                        break;
                    case Expired:
                        break;
                }
            }
        });
        latch.await();
        return zooKeeper;
    }
}

鎖的實現類

package com.sicimike.zk.lock;

import org.apache.zookeeper.ZooKeeper;

/**
 * @author sicimike
 * @create 2020-05-07 21:01
 */
public class ZookeeperLock {


    private String threadName;
    private static ZooKeeper zookeeper;
    LockWatcher lockWatcher;

    static {
        try {
            zookeeper = ZookeeperUtil.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void lock() throws InterruptedException {
        lockWatcher = new LockWatcher();
        lockWatcher.setZooKeeper(zookeeper);
        lockWatcher.setZookeeperLock(this);
        lockWatcher.setThreadName(threadName);
        lockWatcher.lock();
    }

    public void unlock() {
        try {
            lockWatcher.unlock();
            System.out.println(threadName + " unlock");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public ZooKeeper getZookeeper() {
        return zookeeper;
    }

    public void setZookeeper(ZooKeeper zookeeper) {
        this.zookeeper = zookeeper;
    }

    public String getThreadName() {
        return threadName;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }
}

事件處理

package com.sicimike.zk.lock;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * @author sicimike
 * @create 2020-05-07 21:06
 */
public class LockWatcher implements Watcher, AsyncCallback.StringCallback, AsyncCallback.Children2Callback, AsyncCallback.StatCallback {

    private CountDownLatch latch = new CountDownLatch(1);

    private String threadName;
    private String pathName;
    private ZooKeeper zooKeeper;
    private ZookeeperLock zookeeperLock;

    @Override
    public void process(WatchedEvent event) {
        switch (event.getType()) {
            case NodeDeleted:
                zooKeeper.getChildren("/", false, this, "");
                break;
        }
    }

    @Override
    public void processResult(int rc, String path, Object ctx, String name) {
        if (name != null) {
            System.out.println(threadName + " create Znode " + name);
            pathName = name;
            zooKeeper.getChildren("/", false, this, "");
        }
    }

    @Override
    public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
        Collections.sort(children);
        int index = children.indexOf(pathName.substring(1));
        if (index == 0) {
            // 表示自己是最小的節點,相當於自己搶到了鎖
            System.out.println(threadName + " get lock ");
            latch.countDown();
        } else {
            // 如果不是最小的節點,則判斷比自己小的節點是否存在
            zooKeeper.exists(path + children.get(index - 1), this, this, "");
        }
    }

    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {

    }

    public void lock() throws InterruptedException {
        // 創建臨時序列節點
        zooKeeper.create("/lock", threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, "");
        latch.await();
    }

    public void unlock() throws KeeperException, InterruptedException {
        zooKeeper.delete(pathName, -1);
    }

    public ZooKeeper getZooKeeper() {
        return zooKeeper;
    }

    public void setZooKeeper(ZooKeeper zooKeeper) {
        this.zooKeeper = zooKeeper;
    }

    public ZookeeperLock getZookeeperLock() {
        return zookeeperLock;
    }

    public void setZookeeperLock(ZookeeperLock zookeeperLock) {
        this.zookeeperLock = zookeeperLock;
    }

    public String getThreadName() {
        return threadName;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }

    public String getPathName() {
        return pathName;
    }

    public void setPathName(String pathName) {
        this.pathName = pathName;
    }
}

測試代碼

package com.sicimike.zk;

import com.sicimike.zk.lock.ZookeeperLock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author sicimike
 * @create 2020-05-03 12:05
 */
public class ZookeeperDemo {

    private static final int THREAD_COUNT = 6;

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(() -> {
                // 每個線程一把鎖
                ZookeeperLock lock = new ZookeeperLock();
                lock.setThreadName(Thread.currentThread().getName());
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + " working ...");
                    lock.unlock();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        while (true) {}
    }
}

執行結果

pool-1-thread-3 create Znode /lock0000000060
pool-1-thread-4 create Znode /lock0000000061
pool-1-thread-5 create Znode /lock0000000062
pool-1-thread-1 create Znode /lock0000000063
pool-1-thread-6 create Znode /lock0000000064
pool-1-thread-2 create Znode /lock0000000065
pool-1-thread-3 get lock 
pool-1-thread-3 working ...
pool-1-thread-3 unlock
pool-1-thread-4 get lock 
pool-1-thread-4 working ...
pool-1-thread-4 unlock
pool-1-thread-5 get lock 
pool-1-thread-5 working ...
pool-1-thread-5 unlock
pool-1-thread-1 get lock 
pool-1-thread-1 working ...
pool-1-thread-1 unlock
pool-1-thread-6 get lock 
pool-1-thread-6 working ...
pool-1-thread-6 unlock
pool-1-thread-2 get lock 
pool-1-thread-2 working ...
pool-1-thread-2 unlock

從執行結果可以看出,節點的創建順序是固定的,即使每個線程執行的順序不一樣。並且獲取鎖的順序正是Znode被創建的順序。pool-1-thread-3線程在pool-1-thread-4線程之前創建Znode,所以也會先於pool-1-thread-4線程執行。

總結

本篇主要利用了zookeeper集羣實現了分佈式鎖的簡單功能。瞭解分佈式鎖的實現過程,能幫助我們更好的使用分佈式鎖。

參考

  • https://zh.wikipedia.org/wiki/驚羣問題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章