分佈式鎖之zookeeper

引言

在上一篇文章分佈式事務之數據庫鎖中,我們明白了基於mysql數據庫行鎖能完成分佈式事務,同時在最後留了個問題:當某個app加完行鎖後因爲某種原因非正常退出,此時該app並未釋放行鎖,導致其他app沒法獲取鎖,該如何處理?接下來這篇文章將針對這個問題提出我們的解決思路,即採用zookeeper實現分佈式鎖。

解決方案

在實現zookeeper的分佈式鎖前,先理解zookeeper的臨時節點生成分佈式鎖的原理,臨時節點就是zookeeper在創建節點時,該節點是臨時,一旦app和zookeeper的會話結束或者其他網絡等問題超時,zookeeper就會自動刪除該節點,所以叫臨時節點,所有的app在啓動的時候都向zookeeper進行註冊臨時節點,一旦發現有其他節點已經註冊了,那其他節點就等待下去直到該節點的事務處理完釋放鎖,釋放鎖後其他節點就可以向該節點重新競爭獲取鎖,這樣就能保證所有的app在同一個時候只有一個app能獲取鎖,這樣不就解決了我們app忘記釋放鎖導致的問題了。但這裏還有一個問題是,如果我的app有成千上萬個,當鎖釋放後所有的app都會等到該通知,這樣在該瞬時會造成“羊羣效應”,那我們又沒其他的辦法呢?答案是採用zookeeper的臨時順序節點,即zookeeper給每個app分配一個全局唯一的序列號,序列號大的只監聽比其小一號的節點,這樣一旦釋放鎖,只會有一個app得到通知取獲取鎖,這樣就可以解決我們的問題了。

實現方案

下面來看我們的實現:

public class DistributedLock implements Lock, Watcher {
    private ZooKeeper zk = null;
    // 根節點
    private String ROOT_LOCK = "/locks";
    // 競爭的資源
    private String lockName;
    // 等待的前一個鎖
    private String WAIT_LOCK;
    // 當前鎖
    private String CURRENT_LOCK;
    // 計數器
    private CountDownLatch countDownLatch;
    private int sessionTimeout = 30000;
    private List<Exception> exceptionList = new ArrayList<Exception>();

    /**
     * 配置分佈式鎖
     * @param config 連接的url
     * @param lockName 競爭資源
     */
    public DistributedLock(String config, String lockName) {
        this.lockName = lockName;
        try {
            // 連接zookeeper
            zk = new ZooKeeper(config, sessionTimeout, this);
            Stat stat = zk.exists(ROOT_LOCK, false);
            if (stat == null) {
                // 如果根節點不存在,則創建根節點
                zk.create(ROOT_LOCK, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    // 節點監視器
    public void process(WatchedEvent event) {
        if (this.countDownLatch != null) {
            this.countDownLatch.countDown();
        }
    }

    public void lock() {
        if (exceptionList.size() > 0) {
            throw new LockException(exceptionList.get(0));
        }
        try {
            if (this.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " " + lockName + "獲得了鎖");
                return;
            } else {
                // 等待鎖
                waitForLock(WAIT_LOCK, sessionTimeout);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    public boolean tryLock() {
        try {
            String splitStr = "_lock_";
            if (lockName.contains(splitStr)) {
                throw new LockException("鎖名有誤");
            }
            // 創建臨時有序節點
            CURRENT_LOCK = zk.create(ROOT_LOCK + "/" + lockName + splitStr, new byte[0],
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            System.out.println(CURRENT_LOCK + " 已經創建");
            // 取所有子節點
            List<String> subNodes = zk.getChildren(ROOT_LOCK, false);
            // 取出所有lockName的鎖
            List<String> lockObjects = new ArrayList<String>();
            for (String node : subNodes) {
                String _node = node.split(splitStr)[0];
                if (_node.equals(lockName)) {
                    lockObjects.add(node);
                }
            }
            Collections.sort(lockObjects);
            System.out.println(Thread.currentThread().getName() + " 的鎖是 " + CURRENT_LOCK);
            // 若當前節點爲最小節點,則獲取鎖成功
            if (CURRENT_LOCK.equals(ROOT_LOCK + "/" + lockObjects.get(0))) {
                return true;
            }
            // 若不是最小節點,則找到自己的前一個節點
            String prevNode = CURRENT_LOCK.substring(CURRENT_LOCK.lastIndexOf("/") + 1);
            WAIT_LOCK = lockObjects.get(Collections.binarySearch(lockObjects, prevNode) - 1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
        return false;
    }

    public boolean tryLock(long timeout, TimeUnit unit) {
        try {
            if (this.tryLock()) {
                return true;
            }
            return waitForLock(WAIT_LOCK, timeout);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    // 等待鎖
    private boolean waitForLock(String prev, long waitTime) throws KeeperException, InterruptedException {
        Stat stat = zk.exists(ROOT_LOCK + "/" + prev, true);
        if (stat != null) {
            System.out.println(Thread.currentThread().getName() + "等待鎖 " + ROOT_LOCK + "/" + prev);
            this.countDownLatch = new CountDownLatch(1);
            // 計數等待,若等到前一個節點消失,則precess中進行countDown,停止等待,獲取鎖
            this.countDownLatch.await(waitTime, TimeUnit.MILLISECONDS);
            this.countDownLatch = null;
            System.out.println(Thread.currentThread().getName() + " 等到了鎖");
        }
        return true;
    }

    public void unlock() {
        try {
            System.out.println("釋放鎖 " + CURRENT_LOCK);
            zk.delete(CURRENT_LOCK, -1);
            CURRENT_LOCK = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    public Condition newCondition() {
        return null;
    }

    public void lockInterruptibly() throws InterruptedException {
        this.lock();
    }


    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        public LockException(String e){
            super(e);
        }
        public LockException(Exception e){
            super(e);
        }
    }
}
public class LockTest {
    private int n=60; //簡單測試計數器,測試線程在zookeeper鎖後能否是順序執行
    public void secskill(){
        System.out.println(--n);
    }
    @Test
    public void testLock(){
        Runnable runnable1 = new Runnable() {
            public void run() {
                DistributedLock lock = null;
                try {
                    lock = new DistributedLock("10.7.8.102:2181", "test1");
                    lock.lock();//加鎖
                    secskill();//這裏來處理我們的業務邏輯
                    System.out.println(Thread.currentThread().getName() + "正在運行");
                } finally {
                    if (lock != null) {
                        lock.unlock();//業務處理完成後釋放鎖
                    }
                }
            }
        };
         //10個線程併發執行
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable1);
            t.start();//
        }
        //防止主線程退出
        while(true){
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

來看下測試結果:

... ...
Thread-0等待鎖 /locks/test1_lock_0000000044
Thread-4等待鎖 /locks/test1_lock_0000000048
Thread-13等待鎖 /locks/test2_lock_0000000055
Thread-5等待鎖 /locks/test2_lock_0000000050
Thread-9等待鎖 /locks/test2_lock_0000000046
Thread-10等待鎖 /locks/test1_lock_0000000057
Thread-1等待鎖 /locks/test2_lock_0000000049
Thread-18等待鎖 /locks/test1_lock_0000000051
Thread-6等待鎖 /locks/test1_lock_0000000058
57
Thread-17正在運行
釋放鎖 /locks/test2_lock_0000000042
56
Thread-2正在運行
釋放鎖 /locks/test1_lock_0000000044
Thread-3 等到了鎖
55
Thread-3正在運行
釋放鎖 /locks/test2_lock_0000000043
Thread-0 等到了鎖
54
... ...

結論

基於zookeeper能方便的實現分佈式鎖,尤其是解決了會話的超時自動釋放,自動監聽一個節點等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章