阿里JAVA面試題剖析:一般實現分佈式鎖都有哪些方式?使用 Redis 如何設計分佈式鎖?

面試原題

一般實現分佈式鎖都有哪些方式?使用 redis 如何設計分佈式鎖?使用 zk 來設計分佈式鎖可以嗎?這兩種分佈式鎖的實現方式哪種效率比較高?

面試官心理分析

其實一般問問題,都是這麼問的,先問問你 zk,然後其實是要過度到 zk 關聯的一些問題裏去,比如分佈式鎖。因爲在分佈式系統開發中,分佈式鎖的使用場景還是很常見的。
阿里JAVA面試題剖析:一般實現分佈式鎖都有哪些方式?使用 Redis 如何設計分佈式鎖?

面試題剖析

Redis 分佈式鎖

官方叫做 RedLock 算法,是 Redis 官方支持的分佈式鎖算法。

這個分佈式鎖有 3 個重要的考量點:

  • 互斥(只能有一個客戶端獲取鎖)

  • 不能死鎖

  • 容錯(只要大部分 redis 節點創建了這把鎖就可以)

Redis 最普通的分佈式鎖

第一個最普通的實現方式,就是在 redis 裏創建一個 key,這樣就算加鎖。

SETmy:lock隨機值NXPX30000

執行這個命令就 ok。

  • NX:表示只有 key 不存在的時候纔會設置成功。(如果此時 redis 中存在這個 key,那麼設置失敗,返回 nil)

  • PX 30000:意思是 30s 後鎖自動釋放。別人創建的時候如果發現已經有了就不能加鎖了。

釋放鎖就是刪除 key ,但是一般可以用 lua 腳本刪除,判斷 value 一樣才刪除:

-- 刪除鎖的時候,找到 key 對應的 value,跟自己傳過去的 value 做比較,如果是一樣的才刪除。

if redis.call("get",KEYS[1]) == ARGV[1] then

          return redis.call("del",KEYS[1])

else

           return 0

end

爲啥要用隨機值呢?因爲如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執行完,比如說超過了 30s,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除 key 的話會有問題,所以得用隨機值加上面的 lua 腳本來釋放鎖。

但是這樣是肯定不行的。因爲如果是普通的 redis 單實例,那就是單點故障。或者是 redis 普通主從,那 redis 主從異步複製,如果主節點掛了(key 就沒有了),key 還沒同步到從節點,此時從節點切換爲主節點,別人就可以 set key,從而拿到鎖。

RedLock 算法

這個場景是假設有一個 redis cluster,有 5 個 redis master 實例。然後執行如下步驟獲取一把鎖:

獲取當前時間戳,單位是毫秒;

跟上面類似,輪流嘗試在每個 master 節點上創建鎖,過期時間較短,一般就幾十毫秒;

嘗試在大多數節點上建立一個鎖,比如 5 個節點就要求是 3 個節點 n / 2 + 1;

客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了;

要是鎖建立失敗了,那麼就依次之前建立過的鎖刪除;

只要別人建立了一把分佈式鎖,你就得不斷輪詢去嘗試獲取鎖。

阿里JAVA面試題剖析:一般實現分佈式鎖都有哪些方式?使用 Redis 如何設計分佈式鎖?

zk 分佈式鎖

zk 分佈式鎖,其實可以做的比較簡單,就是某個節點嘗試創建臨時 znode,此時創建成功了就獲取了這個鎖;這個時候別的客戶端來創建鎖會失敗,只能註冊個監聽器監聽這個鎖。釋放鎖就是刪除這個 znode,一旦釋放掉就會通知客戶端,然後有一個等待着的客戶端就可以再次重新加鎖。

/**

* ZooKeeperSession

*

* @author bingo

* @since 2018/11/29

*

*/

public class ZooKeeperSession {

    private static CountDownLatch connectedSemaphore = new CountDownLatch(1);

    private ZooKeeper zookeeper;

    private CountDownLatch latch;

    public ZooKeeperSession() {

        try {

            this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher());

            try {

                connectedSemaphore.await();

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println("ZooKeeper session established......");

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    /**

    * 獲取分佈式鎖

    *

    * @param productId

    */

    public Boolean acquireDistributedLock(Long productId) {

        String path = "/product-lock-" + productId;

        try {

            zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

            return true;

        } catch (Exception e) {

            while (true) {

                try {

                    // 相當於是給node註冊一個監聽器,去看看這個監聽器是否存在

                    Stat stat = zk.exists(path, true);

                    if (stat != null) {

                        this.latch = new CountDownLatch(1);

                        this.latch.await(waitTime, TimeUnit.MILLISECONDS);

                        this.latch = null;

                    }

                    zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

                    return true;

                } catch (Exception ee) {

                    continue;

                }

            }

        }

        return true;

    }

    /**

    * 釋放掉一個分佈式鎖

    *

    * @param productId

    */

    public void releaseDistributedLock(Long productId) {

        String path = "/product-lock-" + productId;

        try {

            zookeeper.delete(path, -1);

            System.out.println("release the lock for product[id=" + productId + "]......");

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    /**

    * 建立zk session的watcher

    *

    * @author bingo

    * @since 2018/11/29

    *

    */

    private class ZooKeeperWatcher implements Watcher {

        public void process(WatchedEvent event) {

            System.out.println("Receive watched event: " + event.getState());

            if (KeeperState.SyncConnected == event.getState()) {

                connectedSemaphore.countDown();

            }

            if (this.latch != null) {

                this.latch.countDown();

            }

        }

    }

    /**

    * 封裝單例的靜態內部類

    *

    * @author bingo

    * @since 2018/11/29

    *

    */

    private static class Singleton {

        private static ZooKeeperSession instance;

        static {

            instance = new ZooKeeperSession();

        }

        public static ZooKeeperSession getInstance() {

            return instance;

        }

    }

    /**

    * 獲取單例

    *

    * @return

    */

    public static ZooKeeperSession getInstance() {

        return Singleton.getInstance();

    }

    /**

    * 初始化單例的便捷方法

    */

    public static void init() {

        getInstance();

    }

}

也可以採用另一種方式,創建臨時順序節點:

如果有一把鎖,被多個人給競爭,此時多個人會排隊,第一個拿到鎖的人會執行,然後釋放鎖;後面的每個人都會去監聽排在自己前面的那個人創建的 node 上,一旦某個人釋放了鎖,排在自己後面的人就會被 zookeeper 給通知,一旦被通知了之後,就 ok 了,自己就獲取到了鎖,就可以執行代碼了。

public class ZooKeeperDistributedLock implements Watcher {

    private ZooKeeper zk;

    private String locksRoot = "/locks";

    private String productId;

    private String waitNode;

    private String lockNode;

    private CountDownLatch latch;

    private CountDownLatch connectedLatch = new CountDownLatch(1);

    private int sessionTimeout = 30000;

    public ZooKeeperDistributedLock(String productId) {

        this.productId = productId;

        try {

            String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";

            zk = new ZooKeeper(address, sessionTimeout, this);

            connectedLatch.await();

        } catch (IOException e) {

            throw new LockException(e);

        } catch (KeeperException e) {

            throw new LockException(e);

        } catch (InterruptedException e) {

            throw new LockException(e);

        }

    }

    public void process(WatchedEvent event) {

        if (event.getState() == KeeperState.SyncConnected) {

            connectedLatch.countDown();

            return;

        }

        if (this.latch != null) {

            this.latch.countDown();

        }

    }

    public void acquireDistributedLock() {

        try {

            if (this.tryLock()) {

                return;

            } else {

                waitForLock(waitNode, sessionTimeout);

            }

        } catch (KeeperException e) {

            throw new LockException(e);

        } catch (InterruptedException e) {

            throw new LockException(e);

        }

    }

    public boolean tryLock() {

        try {

    // 傳入進去的locksRoot + “/” + productId

    // 假設productId代表了一個商品id,比如說1

    // locksRoot = locks

    // /locks/10000000000,/locks/10000000001,/locks/10000000002

            lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            // 看看剛創建的節點是不是最小的節點

    // locks:10000000000,10000000001,10000000002

            List<String> locks = zk.getChildren(locksRoot, false);

            Collections.sort(locks);

            if(lockNode.equals(locksRoot+"/"+ locks.get(0))){

                //如果是最小的節點,則表示取得鎖

                return true;

            }

            //如果不是最小的節點,找到比自己小1的節點

  int previousLockIndex = -1;

            for(int i = 0; i < locks.size(); i++) {

if(lockNode.equals(locksRoot + “/” + locks.get(i))) {

            previousLockIndex = i - 1;

    break;

}

  }

  this.waitNode = locks.get(previousLockIndex);

        } catch (KeeperException e) {

            throw new LockException(e);

        } catch (InterruptedException e) {

            throw new LockException(e);

        }

        return false;

    }

    private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {

        Stat stat = zk.exists(locksRoot + "/" + waitNode, true);

        if (stat != null) {

            this.latch = new CountDownLatch(1);

            this.latch.await(waitTime, TimeUnit.MILLISECONDS);

            this.latch = null;

        }

        return true;

    }

    public void unlock() {

        try {

            // 刪除/locks/10000000000節點

            // 刪除/locks/10000000001節點

            System.out.println("unlock " + lockNode);

            zk.delete(lockNode, -1);

            lockNode = null;

            zk.close();

        } catch (InterruptedException e) {

            e.printStackTrace();

        } catch (KeeperException e) {

            e.printStackTrace();

        }

    }

    public class LockException extends RuntimeException {

        private static final long serialVersionUID = 1L;

        public LockException(String e) {

            super(e);

        }

        public LockException(Exception e) {

            super(e);

        }

    }

}

Redis 分佈式鎖和 zk 分佈式鎖的對比

Redis 分佈式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能。

zk 分佈式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小。

另外一點就是,如果是 redis 獲取鎖的那個客戶端 出現 bug 掛了,那麼只能等待超時時間之後才能釋放鎖;而 zk 的話,因爲創建的是臨時 znode,只要客戶端掛了,znode 就沒了,此時就自動釋放鎖。

Redis 分佈式鎖大家沒發現好麻煩嗎?遍歷上鎖,計算時間等等......zk 的分佈式鎖語義清晰實現簡單。

所以先不分析太多的東西,就說這兩點,我個人實踐認爲 zk 的分佈式鎖比 redis 的分佈式鎖牢靠、而且模型簡單易用。


文末彩蛋

針對於上面所涉及到的知識點我總結出了有1到5年開發經驗的程序員在面試中涉及到的絕大部分架構面試題及答案做成了文檔和架構視頻資料免費分享給大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術資料),希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習,也可以關注我一下以後會有更多幹貨分享。

資料獲取方式 QQ羣搜索“708-701-457” 即可免費領取

阿里JAVA面試題剖析:一般實現分佈式鎖都有哪些方式?使用 Redis 如何設計分佈式鎖?
阿里JAVA面試題剖析:一般實現分佈式鎖都有哪些方式?使用 Redis 如何設計分佈式鎖?
阿里JAVA面試題剖析:一般實現分佈式鎖都有哪些方式?使用 Redis 如何設計分佈式鎖?

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