1. 程式人生 > >阿裏JAVA面試題剖析:一般實現分布式鎖都有哪些方式?使用 Redis 如何設計分布式鎖?

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

自己 ini 單位 nts ast 客戶端 this 失敗 獲取

面試原題

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

面試官心理分析

其實一般問問題,都是這麽問的,先問問你 zk,然後其實是要過度到 zk 關聯的一些問題裏去,比如分布式鎖。因為在分布式系統開發中,分布式鎖的使用場景還是很常見的。
技術分享圖片

面試題剖析

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;

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

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

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

技術分享圖片

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 的分布式鎖牢靠、而且模型簡單易用。

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