Zookeeper 實(shí)現(xiàn)分布式節(jié)點(diǎn)下的配置文件統(tǒng)一管理和分布式鎖

一陪白、ZooKeeper 簡介

ZooKeeper 是一個(gè)集中式服務(wù),用于維護(hù)配置信息启搂,命名潮售,提供分布式同步和提供組服務(wù)。
ZooKeeper 的主要應(yīng)用:1动壤、節(jié)點(diǎn)選舉萝喘;2、配置文件統(tǒng)一管理琼懊;3阁簸、分布式鎖;4哼丈、發(fā)布與訂閱(Dubbo)启妹;5、集群管理醉旦,集群中保證數(shù)據(jù)的強(qiáng)一致性饶米,下面我們主要講配置文件統(tǒng)一管理和分布式鎖。

Zookeeper文件系統(tǒng)

Zookeeper的每個(gè)子目錄項(xiàng)如 NameService 都被稱作為 znode车胡,和文件系統(tǒng)一樣檬输,我們能夠自由的增加、刪除 znode匈棘,在一個(gè) znode 下增加丧慈、刪除子 znode,唯一的不同在于znode是可以存儲(chǔ)數(shù)據(jù)的主卫。

有四種類型的 znode:

1逃默、PERSISTENT-持久化目錄節(jié)點(diǎn)
客戶端與 zookeeper 斷開連接后,該節(jié)點(diǎn)依舊存在簇搅。
2完域、PERSISTENT_SEQUENTIAL-持久化順序編號(hào)目錄節(jié)點(diǎn)
客戶端與 zookeeper 斷開連接后,該節(jié)點(diǎn)依舊存在瘩将,只是 zookeeper 給該節(jié)點(diǎn)名稱進(jìn)行順序編號(hào)吟税。
3关噪、EPHEMERAL-臨時(shí)目錄節(jié)點(diǎn)
客戶端與 zookeeper 斷開連接后,該節(jié)點(diǎn)被刪除乌妙。
4使兔、EPHEMERAL_SEQUENTIAL-臨時(shí)順序編號(hào)目錄節(jié)點(diǎn)
客戶端與 zookeeper 斷開連接后,該節(jié)點(diǎn)被刪除藤韵,只是 zookeeper 給該節(jié)點(diǎn)名稱進(jìn)行順序編號(hào)虐沥。

二、配置文件統(tǒng)一管理

1泽艘、實(shí)現(xiàn)思路

假如我們需要修改三(或者更多)臺(tái)服務(wù)器上 redis.conf 的配置信息欲险,如果一臺(tái)一臺(tái)的去修改,則會(huì)加大出錯(cuò)概率匹涮,而且也不實(shí)際天试。這時(shí)候,我們需要引入Zookeeper(下面簡稱zk)然低,我們需要知道喜每,zk 中有個(gè) watcher 事件,包括 :
EventType:NodeCreated //節(jié)點(diǎn)創(chuàng)建
EventType:NodeDataChanged //節(jié)點(diǎn)的數(shù)據(jù)變更
EventType:NodeChildrentChanged //子節(jié)點(diǎn)下的數(shù)據(jù)變更
EventType:NodeDeleted // 節(jié)點(diǎn)刪除
當(dāng)我們監(jiān)聽了上面的事件時(shí)雳攘,事件觸發(fā)就會(huì)被告知带兜。以統(tǒng)一更新 redis.conf 配置文件為例,我們可以實(shí)現(xiàn)監(jiān)聽某一個(gè)節(jié)點(diǎn)的數(shù)據(jù)更新事件吨灭,當(dāng)DBA更改了該節(jié)點(diǎn)的值(一般為 json 串刚照,方便程序解析,例:{"type":"update","url":"ftp:192.168.2.10/config/redis.xml"})喧兄,此時(shí)我們可以根據(jù) type 的值“update”可知无畔,是需要更新 redis.conf 配置文件,然后根據(jù) url 的值吠冤,獲取最新的 redis.conf 文件所在的服務(wù)器地址浑彰。此時(shí),我們可以下載最新配置文件咨演,然后刪除原來的 redis.conf 配置文件闸昨,最后將最新的配置文件添加到項(xiàng)目中蚯斯,從而通過重啟程序就可以讀取到最新的配置了薄风。

2、代碼實(shí)現(xiàn)

這里我們模擬了三個(gè)客戶端 Client1拍嵌、Client2遭赂、Client3,代碼都是一樣的横辆。當(dāng) zk 節(jié)點(diǎn)數(shù)據(jù)發(fā)送變化撇他,就會(huì)觸發(fā)數(shù)據(jù)更新的事件,從而告知其客戶端(必須監(jiān)聽了該事件)。

package com.imooc.curator.operator;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.retry.RetryNTimes;
import org.springframework.util.StringUtils;

import java.util.concurrent.CountDownLatch;

/**
 *  使用 zk 的 watch 事件困肩,實(shí)現(xiàn)配置文件的統(tǒng)一配置
 * @author K. L. Mao
 * @create 2018/9/8
 */
public class Client1 {
    public CuratorFramework client;
    public static final String zkServerPath = "192.168.174.10:2181,192.168.174.11:2181,192.168.174.12:2181";

    public static final String CONFIG_NODE_PATH = "/super/imooc";
    public static final String SUB_PATH = "/redis-config";
    public static CountDownLatch countDownLatch = new CountDownLatch(1);

    /**
     * 實(shí)例化 zk 客戶端
     */
    public Client1(){
        /**
         * curator 連接 zk 的策略:RetryNTimes
         * n:重試次數(shù)
         * sleepMsBetweenRetries:每次重試間隔的時(shí)間
         */
        RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
        client = CuratorFrameworkFactory.builder()
                .connectString(zkServerPath)
                .sessionTimeoutMs(10000).retryPolicy(retryPolicy)
                .namespace("workspace").build();
        // 啟動(dòng)客戶端連接
        client.start();
    }

    /**
     * 關(guān)閉 zk 客戶端
     */
    public void closeZKClient() {
        if (client != null){
            client.close();
        }
    }

    public static void main(String[] args) throws Exception {
        // 實(shí)例化
        Client1 operator = new Client1();
        System.out.println("client1 啟動(dòng)成功");
        // 創(chuàng)建節(jié)點(diǎn)
//        byte[] data = "super".getBytes();
//        operator.client.create().creatingParentsIfNeeded()
//                .withMode(CreateMode.PERSISTENT)    // 持久化節(jié)點(diǎn)
//                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)   // 全權(quán)限ACL
//                .forPath(nodePath, data);

        // watcher 事件 當(dāng)使用usingWatcher的時(shí)候划纽,監(jiān)聽只會(huì)觸發(fā)一次,監(jiān)聽完畢后就銷毀
//        operator.client.getData().usingWatcher(new MyCuratorWatcher()).forPath(nodePath);
        // NodeCache:監(jiān)聽 CONFIG_NODE_PATH 下的子包數(shù)據(jù)節(jié)點(diǎn)的變更锌畸,會(huì)觸發(fā)事件
        final PathChildrenCache nodeCache = new PathChildrenCache(operator.client, CONFIG_NODE_PATH, true);
        // 初始化的時(shí)候獲取 node 的值并且緩存
        nodeCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
        // 新增監(jiān)聽器
        nodeCache.getListenable().addListener((client, event) -> {
            // 監(jiān)聽節(jié)點(diǎn)更新
            if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
                String configNodePath = event.getData().getPath();
                if (configNodePath.equals(CONFIG_NODE_PATH + SUB_PATH)){
                    System.out.println("監(jiān)聽到配置發(fā)生變化勇劣,節(jié)點(diǎn)路徑為:" + configNodePath);
                    // 讀取節(jié)點(diǎn)數(shù)據(jù)
                    String data = new String(event.getData().getData(), "UTF-8");
                    System.out.println("節(jié)點(diǎn)" + CONFIG_NODE_PATH + "的數(shù)據(jù)為" + data);
                    if (!StringUtils.isEmpty(data)){
                        JSONObject jsonObject = JSON.parseObject(data);
                        String type = jsonObject.getString("type");
                        String url = jsonObject.getString("url");
                        if ("add".equals(type)){
                            System.out.println("監(jiān)聽到新增的配置,準(zhǔn)備下載...");
                            // ... 連接ftp服務(wù)器潭枣,根據(jù)url找到對(duì)應(yīng)的配置
                            Thread.sleep(500);
                            System.out.println("開始下載新的配置文件比默,下載路徑為<" + url + ">");
                            // ... 下載配置到你指定的目錄
                            Thread.sleep(1000);
                            System.out.println("下載成功,已經(jīng)添加到項(xiàng)目中");
                        }else if ("update".equals(type)){
                            System.out.println("監(jiān)聽到更新的配置盆犁,準(zhǔn)備下載...");
                            // ... 連接ftp服務(wù)器命咐,根據(jù)url找到對(duì)應(yīng)的配置
                            Thread.sleep(500);
                            System.out.println("開始下載新的配置文件,下載路徑為<" + url + ">");
                            // ... 下載配置到你指定的目錄
                            Thread.sleep(1000);
                            System.out.println("下載成功");
                            System.out.println("刪除項(xiàng)目中原配置文件...");
                            Thread.sleep(100);
                            // ... 刪除原文件
                            System.out.println("拷貝配置文件到項(xiàng)目目錄...");
                            // ... 拷貝文件到項(xiàng)目
                        }else if ("delete".equals(type)){
                            System.out.println("監(jiān)聽到刪除的配置");
                            System.out.println("刪除項(xiàng)目中原配置文件...");
                        }
                    }
                }
            }
        });
        countDownLatch.await();
        operator.closeZKClient();
    }
}

三谐岁、分布式鎖

1醋奠、場(chǎng)景

高并場(chǎng)景下,對(duì)共享資源的訪問伊佃,都需要加鎖钝域,分布式的環(huán)境下,就需要加分布式鎖锭魔。如下代碼例证,模擬一個(gè)分布式的并發(fā)操作。

  • Service
package com.imooc.curator.service;

import org.springframework.stereotype.Service;

/**
 * @author K. L. Mao
 * @create 2018/9/9
 */
@Service
public class PayService {

    private static int COUNT = 100;

    /**
     * 高并發(fā)下的 count-1
     * @return
     */
    public int countLock(){
        if (COUNT <= 99){
            return -1;
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        COUNT = COUNT - 1;
        return COUNT;
    }
}
  • Controller
package com.imooc.curator.web;

import com.imooc.curator.service.PayService;
import com.imooc.curator.utils.ZKCurator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author K. L. Mao
 * @create 2018/9/9
 */
@RestController
public class PayController {

    @Autowired
    private PayService payService;

    /**
     * 模擬客戶端1
     * @return
     */
    @GetMapping("/lock")
    public int lock(){
       return payService.countLock();
    }

    /**
     * 模擬客戶端2
     * @return
     */
    @GetMapping("/lock2")
    public int lock2(){
        return payService.countLock();
    }

}

當(dāng)我們?cè)谌雰?nèi)分別訪問 localhost:8080/lock 和 localhost:8080/lock2 時(shí)迷捧,就會(huì)產(chǎn)生2個(gè)線程對(duì) COUNT 進(jìn)行了操作织咧,使其 變?yōu)?8了。這樣是不允許的漠秋,那么如何保證這兩個(gè)線程是依次執(zhí)行的呢笙蒙?這時(shí)候,我們需要 zk 來實(shí)現(xiàn)分布式鎖了庆锦。

2捅位、實(shí)現(xiàn)思路

首先,Zookeeper 的每一個(gè)節(jié)點(diǎn)搂抒,都是一個(gè)天然的順序發(fā)號(hào)器艇搀。

在每一個(gè)節(jié)點(diǎn)下面創(chuàng)建子節(jié)點(diǎn)時(shí),只要選擇的創(chuàng)建類型是有序(EPHEMERAL_SEQUENTIAL 臨時(shí)有序或者PERSISTENT_SEQUENTIAL 永久有序)類型求晶,那么焰雕,新的子節(jié)點(diǎn)后面,會(huì)加上一個(gè)次序編號(hào)芳杏。這個(gè)次序編號(hào)矩屁,是上一個(gè)生成的次序編號(hào)加一辟宗。

比如,創(chuàng)建一個(gè)用于發(fā)號(hào)的節(jié)點(diǎn)“/test/lock”吝秕,然后以他為父親節(jié)點(diǎn)泊脐,可以在這個(gè)父節(jié)點(diǎn)下面創(chuàng)建相同前綴的子節(jié)點(diǎn),假定相同的前綴為“/test/lock/seq-”烁峭,在創(chuàng)建子節(jié)點(diǎn)時(shí)晨抡,同時(shí)指明是有序類型。如果是第一個(gè)創(chuàng)建的子節(jié)點(diǎn)则剃,那么生成的子節(jié)點(diǎn)為“/test/lock/seq-0000000000”耘柱,下一個(gè)節(jié)點(diǎn)則為“/test/lock/seq-0000000001”,依次類推棍现,等等调煎。



其次,Zookeeper節(jié)點(diǎn)的遞增性己肮,可以規(guī)定節(jié)點(diǎn)編號(hào)最小的那個(gè)獲得鎖士袄。

一個(gè)zookeeper分布式鎖,首先需要?jiǎng)?chuàng)建一個(gè)父節(jié)點(diǎn)谎僻,盡量是持久節(jié)點(diǎn)(PERSISTENT類型)娄柳,然后每個(gè)要獲得鎖的線程都會(huì)在這個(gè)節(jié)點(diǎn)下創(chuàng)建個(gè)臨時(shí)順序節(jié)點(diǎn),由于序號(hào)的遞增性艘绍,可以規(guī)定排號(hào)最小的那個(gè)獲得鎖赤拒。所以,每個(gè)線程在嘗試占用鎖之前诱鞠,首先判斷自己是排號(hào)是不是當(dāng)前最小挎挖,如果是,則獲取鎖航夺。

第三蕉朵,Zookeeper的節(jié)點(diǎn)監(jiān)聽機(jī)制,可以保障占有鎖的方式有序而且高效阳掐。

每個(gè)線程搶占鎖之前始衅,先搶號(hào)創(chuàng)建自己的ZNode。同樣缭保,釋放鎖的時(shí)候汛闸,就需要?jiǎng)h除搶號(hào)的Znode。搶號(hào)成功后涮俄,如果不是排號(hào)最小的節(jié)點(diǎn)蛉拙,就處于等待通知的狀態(tài)尸闸。等誰的通知呢彻亲?不需要其他人孕锄,只需要等前一個(gè)Znode 的通知就可以了。當(dāng)前一個(gè)Znode 刪除的時(shí)候苞尝,就是輪到了自己占有鎖的時(shí)候畸肆。第一個(gè)通知第二個(gè)、第二個(gè)通知第三個(gè)宙址,擊鼓傳花似的依次向后轴脐。

Zookeeper的節(jié)點(diǎn)監(jiān)聽機(jī)制,可以說能夠非常完美的抡砂,實(shí)現(xiàn)這種擊鼓傳花似的信息傳遞大咱。具體的方法是,每一個(gè)等通知的Znode節(jié)點(diǎn)注益,只需要監(jiān)聽或者 watch 監(jiān)視排號(hào)在自己前面那個(gè)碴巾,而且緊挨在自己前面的那個(gè)節(jié)點(diǎn)。 只要上一個(gè)節(jié)點(diǎn)被刪除了丑搔,就進(jìn)行再一次判斷厦瓢,看看自己是不是序號(hào)最小的那個(gè)節(jié)點(diǎn),如果是啤月,則獲得鎖煮仇。

3、代碼實(shí)現(xiàn)

首先定義了一個(gè)鎖的接口谎仲,很簡單浙垫,一個(gè)加鎖方法,一個(gè)解鎖方法郑诺。

public interface Lock {

    boolean lock() throws Exception;

    boolean unlock();
}
  • lock 加鎖方法
    @Override
    public boolean lock() {

       try {
            boolean locked = false;

            // 嘗試加鎖
            locked = tryLock();

            if (locked) {
                return true;
            }
            while (!locked) {

                // 加鎖失敗绞呈,則等待
                await();


                if (checkLocked()) {
                    locked=true;
                }
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            unlock();
        }

        return false;
    }

lock方法的具體邏輯是,首先嘗試著去加鎖间景,如果加鎖失敗就去等待佃声,然后再重復(fù)。嘗試加鎖的tryLock方法是關(guān)鍵倘要。做了兩件重要的事情:

(1)創(chuàng)建臨時(shí)順序節(jié)點(diǎn)圾亏,并且保存自己的節(jié)點(diǎn)路徑。

(2)判斷是否是第一個(gè)封拧,如果是第一個(gè)志鹃,則加鎖成功。如果不是泽西,就找到前一個(gè)Znode節(jié)點(diǎn)曹铃,并且保存其路徑到 prior_path。

  • tryLock 方法:
private boolean tryLock() throws Exception {
        //創(chuàng)建臨時(shí)Znode
        List<String> waiters = getWaiters();
        locked_path = ZKclient.instance
                .createEphemeralSeqNode(LOCK_PREFIX);
        if (null == locked_path) {
            throw new Exception("zk error");
        }
        locked_short_path = getShorPath(locked_path);

        //獲取等待的子節(jié)點(diǎn)列表捧杉,判斷自己是否第一個(gè)
        if (checkLocked()) {
            return true;
        }

        // 判斷自己排第幾個(gè)
        int index = Collections.binarySearch(waiters, locked_short_path);
        if (index < 0) { // 網(wǎng)絡(luò)抖動(dòng)陕见,獲取到的子節(jié)點(diǎn)列表里可能已經(jīng)沒有自己了
            throw new Exception("節(jié)點(diǎn)沒有找到: " + locked_short_path);
        }

        //如果自己沒有獲得鎖秘血,則要監(jiān)聽前一個(gè)節(jié)點(diǎn)
        prior_path = ZK_PATH + "/" + waiters.get(index - 1);

        return false;
    }

創(chuàng)建臨時(shí)順序節(jié)點(diǎn)后,其完整路徑存放在locked_path成員變量中评甜。另外還截取了一個(gè)后綴路徑灰粮,放在 locked_short_path成員變量中。 這個(gè)后綴路徑忍坷,是一個(gè)短路徑粘舟,只有完整路徑的最后一層。在和取到的遠(yuǎn)程子節(jié)點(diǎn)列表中的其他路徑進(jìn)行比較時(shí)佩研,需要用到短路徑柑肴。因?yàn)樽庸?jié)點(diǎn)列表的路徑,都是短路徑旬薯,只有最后一層嘉抒。

然后,調(diào)用checkLocked方法袍暴,判斷是否是鎖定成功些侍。如果是則返回。如果自己沒有獲得鎖政模,則要監(jiān)聽前一個(gè)節(jié)點(diǎn)岗宣。找出前一個(gè)節(jié)點(diǎn)的路徑,保存在prior_path成員中淋样,供后面的await等待方法耗式,去監(jiān)聽使用。

在進(jìn)入await等待方法的介紹前趁猴,先說下checkLocked鎖定判斷方法刊咳。

checkLocked方法中,判斷是否可以持有鎖儡司。判斷規(guī)則很簡單:當(dāng)前創(chuàng)建的節(jié)點(diǎn)娱挨,是否在上一步獲取到的子節(jié)點(diǎn)列表的第一個(gè)位置:

如果是,說明可以持有鎖捕犬,返回true跷坝,表示加鎖成功;

如果不是碉碉,說明有其他線程早已先持有了鎖柴钻,返回false。

  • checkLocked 方法
private boolean checkLocked() {
        //獲取等待的子節(jié)點(diǎn)列表

        List<String> waiters = getWaiters();
        //節(jié)點(diǎn)按照編號(hào)垢粮,升序排列
        Collections.sort(waiters);

        // 如果是第一個(gè)贴届,代表自己已經(jīng)獲得了鎖
        if (locked_short_path.equals(waiters.get(0))) {
            log.info("成功的獲取分布式鎖,節(jié)點(diǎn)為{}", locked_short_path);
            return true;
        }
        return false;
    }

等待方法await,表示在爭(zhēng)奪鎖失敗以后的等待邏輯。那么此處該線程應(yīng)該做什么呢毫蚓?

  • await 方法
private void await() throws Exception {

        if (null == prior_path) {
            throw new Exception("prior_path error");
        }

        final CountDownLatch latch = new CountDownLatch(1);


        //訂閱比自己次小順序節(jié)點(diǎn)的刪除事件
        Watcher w = new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                System.out.println("監(jiān)聽到的變化 watchedEvent = " + watchedEvent);
                log.info("[WatchedEvent]節(jié)點(diǎn)刪除");

                latch.countDown();
            }
        };

        client.getData().usingWatcher(w).forPath(prior_path);
      
        latch.await(WAIT_TIME, TimeUnit.SECONDS);
    }

首先添加一個(gè)watcher監(jiān)聽占键,而監(jiān)聽的地址正是上面一步返回的prior_path成員。這里绍些,僅僅會(huì)監(jiān)聽自己前一個(gè)節(jié)點(diǎn)的變動(dòng)捞慌,而不是父節(jié)點(diǎn)下所有節(jié)點(diǎn)的變動(dòng)耀鸦。然后柬批,調(diào)用latch.await,進(jìn)入等待狀態(tài)袖订,等到latch.countDown()被喚醒氮帐。

一旦prior_path節(jié)點(diǎn)發(fā)生了變動(dòng),那么就將線程從等待狀態(tài)喚醒洛姑,重新一輪的鎖的爭(zhēng)奪上沐。

至此,關(guān)于加鎖的算法基本完成楞艾。但是参咙,上面還沒有實(shí)現(xiàn)鎖的可重入。

  • 可重入加鎖方式
#修改前面的lock方法硫眯,在前面加上可重入的判斷邏輯蕴侧。代碼如下:

  public boolean lock() {
     synchronized (this) {
        if (lockCount.get() == 0) {
            thread = Thread.currentThread();
            lockCount.incrementAndGet();
        } else {
            if (!thread.equals(Thread.currentThread())) {
                return false;
            }
            lockCount.incrementAndGet();
            return true;
        }
    }
    
   //...
   }

為了變成可重入,在代碼中增加了一個(gè)加鎖的計(jì)數(shù)器lockCount两入,計(jì)算重復(fù)加鎖的次數(shù)净宵。如果是同一個(gè)線程加鎖,只需要增加次數(shù)裹纳,直接返回择葡,表示加鎖成功。

  • unlock 方法

釋放鎖主要有兩個(gè)工作:

(1)減少重入鎖的計(jì)數(shù)剃氧,如果不是0敏储,直接返回,表示成功的釋放了一次朋鞍;

(2)如果計(jì)數(shù)器為0虹曙,移除Watchers監(jiān)聽器,并且刪除創(chuàng)建的 Znode 臨時(shí)節(jié)點(diǎn)番舆;

代碼如下:

    @Override
    public boolean unlock() {

        if (!thread.equals(Thread.currentThread())) {
            return false;
        }

        int newLockCount = lockCount.decrementAndGet();

        if (newLockCount < 0) {
            throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + locked_path);
        }

        if (newLockCount != 0) {
            return true;
        }
        try {
            if (ZKclient.instance.isNodeExist(locked_path)) {
                client.delete().forPath(locked_path);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

這里酝碳,為了盡量保證線程安全,可重入計(jì)數(shù)器的類型恨狈,不是 int 類型疏哗,而是Java并發(fā)包中的原子類型——AtomicInteger

4禾怠、分布式鎖的應(yīng)用場(chǎng)景

前面的實(shí)現(xiàn)返奉,主要的價(jià)值是展示一下分布式鎖的基礎(chǔ)開發(fā)和原理贝搁。實(shí)際的開發(fā)中,如果需要使用到分布式鎖芽偏,并不需要自己造輪子雷逆,可以直接使用curator客戶端中的各種官方實(shí)現(xiàn)的分布式鎖,比如其中的InterProcessMutex可重入鎖污尉。

  • InterProcessMutex 可重入鎖的使用實(shí)例如下:
@Test
public void testzkMutex() throws InterruptedException {

    CuratorFramework client=ZKclient.instance.getClient();
    final InterProcessMutex zkMutex =
            new InterProcessMutex(client,"/mutex");  ;
    for (int i = 0; i < 10; i++) {
        FutureTaskScheduler.add(() -> {

            try {
                zkMutex.acquire();

                for (int j = 0; j < 10; j++) {

                    count++;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("count = " + count);
                zkMutex.release();

            } catch (Exception e) {
                e.printStackTrace();
            }

        });
    }

    Thread.sleep(Integer.MAX_VALUE);
}

最后膀哲,總結(jié)一下 Zookeeper 分布式鎖。

利用在同級(jí)目錄下被碗,不能創(chuàng)建相同的節(jié)點(diǎn)特性某宪,可以利用多線程去創(chuàng)建一個(gè)節(jié)點(diǎn),但是只能有一個(gè)線程可以創(chuàng)建成功锐朴,所以該線程得到鎖兴喂。釋放鎖:釋放鎖時(shí),通過刪除該節(jié)點(diǎn)焚志,來觸發(fā)剛才沒有獲取到的線程的監(jiān)聽衣迷,讓他們?cè)俅蝸砀?jìng)爭(zhēng)獲取。這種方案可以達(dá)到效果酱酬,但是會(huì)有一個(gè)問題產(chǎn)生壶谒,就是如果在并發(fā)比較大的情況下,一個(gè)臨時(shí)節(jié)點(diǎn)的消失岳悟,會(huì)造成很多線程同時(shí)會(huì)試圖創(chuàng)建臨時(shí)節(jié)點(diǎn)佃迄,這種方式會(huì)影響 zk 的穩(wěn)定性,這個(gè)效應(yīng)稱為羊群效應(yīng)贵少。

Zookeeper分布式鎖呵俏,能有效的解決分布式問題,不可重入問題滔灶,實(shí)現(xiàn)起來較為簡單普碎。

但是,Zookeeper 實(shí)現(xiàn)的分布式鎖其實(shí)存在一個(gè)缺點(diǎn)录平,那就是性能并不太高麻车。因?yàn)槊看卧趧?chuàng)建鎖和釋放鎖的過程中,都要?jiǎng)討B(tài)創(chuàng)建斗这、銷毀臨時(shí)節(jié)點(diǎn)來實(shí)現(xiàn)鎖功能动猬。ZK中創(chuàng)建和刪除節(jié)點(diǎn)只能通過 Leader 服務(wù)器來執(zhí)行,然后 Leader 服務(wù)器還需要將數(shù)據(jù)同步到所有的 Follower 機(jī)器上表箭。所以赁咙,在高性能,高并發(fā)的場(chǎng)景下,不建議使用Zk的分布式鎖彼水,建議使用 Redis崔拥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市凤覆,隨后出現(xiàn)的幾起案子链瓦,更是在濱河造成了極大的恐慌,老刑警劉巖盯桦,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慈俯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡俺附,警方通過查閱死者的電腦和手機(jī)肥卡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門溪掀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來事镣,“玉大人,你說我怎么就攤上這事揪胃×в矗” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵喊递,是天一觀的道長随闪。 經(jīng)常有香客問我,道長骚勘,這世上最難降的妖魔是什么铐伴? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮俏讹,結(jié)果婚禮上当宴,老公的妹妹穿的比我還像新娘。我一直安慰自己泽疆,他們只是感情好户矢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著殉疼,像睡著了一般梯浪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓢娜,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天挂洛,我揣著相機(jī)與錄音,去河邊找鬼眠砾。 笑死虏劲,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伙单,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼获高,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了吻育?” 一聲冷哼從身側(cè)響起念秧,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎布疼,沒想到半個(gè)月后摊趾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡游两,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年砾层,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贱案。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肛炮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宝踪,到底是詐尸還是另有隱情侨糟,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布瘩燥,位于F島的核電站秕重,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏厉膀。R本人自食惡果不足惜溶耘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望服鹅。 院中可真熱鬧凳兵,春花似錦、人聲如沸菱魔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澜倦。三九已至聚蝶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間藻治,已是汗流浹背碘勉。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桩卵,地道東北人验靡。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓倍宾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親胜嗓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子高职,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容