阿里JAVA面試題剖析:一般實(shí)現(xiàn)分布式鎖都有哪些方式?使用 Redis 如何設(shè)計(jì)分布式鎖?使用 zk 來設(shè)計(jì)分布式鎖可以嗎?這兩種分布式鎖的實(shí)現(xiàn)方式哪種效率比較高?

面試原題

一般實(shí)現(xiàn)分布式鎖都有哪些方式?使用 redis 如何設(shè)計(jì)分布式鎖?使用 zk 來設(shè)計(jì)分布式鎖可以嗎?這兩種分布式鎖的實(shí)現(xiàn)方式哪種效率比較高?

面試官心理分析

其實(shí)一般問問題,都是這么問的,先問問你 zk,然后其實(shí)是要過度到 zk 關(guān)聯(lián)的一些問題里去,比如分布式鎖。因?yàn)樵诜植际较到y(tǒng)開發(fā)中,分布式鎖的使用場景還是很常見的。

面試題剖析

redis 分布式鎖

官方叫做?RedLock?算法,是 redis 官方支持的分布式鎖算法。

這個(gè)分布式鎖有 3 個(gè)重要的考量點(diǎn):

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

不能死鎖

容錯(cuò)(只要大部分 redis 節(jié)點(diǎn)創(chuàng)建了這把鎖就可以)

redis 最普通的分布式鎖

第一個(gè)最普通的實(shí)現(xiàn)方式,就是在 redis 里創(chuàng)建一個(gè) key,這樣就算加鎖。

SETmy:lock隨機(jī)值NXPX30000

執(zhí)行這個(gè)命令就 ok。

NX:表示只有?key?不存在的時(shí)候才會設(shè)置成功。(如果此時(shí) redis 中存在這個(gè) key,那么設(shè)置失敗,返回?nil)

PX 30000:意思是 30s 后鎖自動(dòng)釋放。別人創(chuàng)建的時(shí)候如果發(fā)現(xiàn)已經(jīng)有了就不能加鎖了。

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

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

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

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

else

? ? ? ? ? ?return 0

end

為啥要用隨機(jī)值呢?因?yàn)槿绻硞€(gè)客戶端獲取到了鎖,但是阻塞了很長時(shí)間才執(zhí)行完,比如說超過了 30s,此時(shí)可能已經(jīng)自動(dòng)釋放鎖了,此時(shí)可能別的客戶端已經(jīng)獲取到了這個(gè)鎖,要是你這個(gè)時(shí)候直接刪除 key 的話會有問題,所以得用隨機(jī)值加上面的?lua?腳本來釋放鎖。

但是這樣是肯定不行的。因?yàn)槿绻瞧胀ǖ?redis 單實(shí)例,那就是單點(diǎn)故障?;蛘呤?redis 普通主從,那 redis 主從異步復(fù)制,如果主節(jié)點(diǎn)掛了(key 就沒有了),key 還沒同步到從節(jié)點(diǎn),此時(shí)從節(jié)點(diǎn)切換為主節(jié)點(diǎn),別人就可以 set key,從而拿到鎖。

RedLock 算法

這個(gè)場景是假設(shè)有一個(gè) redis cluster,有 5 個(gè) redis master 實(shí)例。然后執(zhí)行如下步驟獲取一把鎖:

獲取當(dāng)前時(shí)間戳,單位是毫秒;

跟上面類似,輪流嘗試在每個(gè) master 節(jié)點(diǎn)上創(chuàng)建鎖,過期時(shí)間較短,一般就幾十毫秒;

嘗試在大多數(shù)節(jié)點(diǎn)上建立一個(gè)鎖,比如 5 個(gè)節(jié)點(diǎn)就要求是 3 個(gè)節(jié)點(diǎn)?n / 2 + 1;

客戶端計(jì)算建立好鎖的時(shí)間,如果建立鎖的時(shí)間小于超時(shí)時(shí)間,就算建立成功了;

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

只要?jiǎng)e人建立了一把分布式鎖,你就得不斷輪詢?nèi)L試獲取鎖。

zk 分布式鎖

zk 分布式鎖,其實(shí)可以做的比較簡單,就是某個(gè)節(jié)點(diǎn)嘗試創(chuàng)建臨時(shí) znode,此時(shí)創(chuàng)建成功了就獲取了這個(gè)鎖;這個(gè)時(shí)候別的客戶端來創(chuàng)建鎖會失敗,只能注冊個(gè)監(jiān)聽器監(jiān)聽這個(gè)鎖。釋放鎖就是刪除這個(gè) znode,一旦釋放掉就會通知客戶端,然后有一個(gè)等待著的客戶端就可以再次重新加鎖。

/**

* 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 {

? ? ? ? ? ? ? ? ? ? // 相當(dāng)于是給node注冊一個(gè)監(jiān)聽器,去看看這個(gè)監(jiān)聽器是否存在

? ? ? ? ? ? ? ? ? ? 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;

? ? }

? ? /**

? ?? * 釋放掉一個(gè)分布式鎖

? ?? *

? ?? * @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();

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ?? * 封裝單例的靜態(tài)內(nèi)部類

? ?? *

? ?? * @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();

? ? }

}

也可以采用另一種方式,創(chuàng)建臨時(shí)順序節(jié)點(diǎn):

如果有一把鎖,被多個(gè)人給競爭,此時(shí)多個(gè)人會排隊(duì),第一個(gè)拿到鎖的人會執(zhí)行,然后釋放鎖;后面的每個(gè)人都會去監(jiān)聽排在自己前面的那個(gè)人創(chuàng)建的 node 上,一旦某個(gè)人釋放了鎖,排在自己后面的人就會被 zookeeper 給通知,一旦被通知了之后,就 ok 了,自己就獲取到了鎖,就可以執(zhí)行代碼了。

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 {

? ? // 傳入進(jìn)去的locksRoot + “/” + productId

? ? // 假設(shè)productId代表了一個(gè)商品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);

? ? ? ? ? ? // 看看剛創(chuàng)建的節(jié)點(diǎn)是不是最小的節(jié)點(diǎn)

? ? // locks:10000000000,10000000001,10000000002

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

? ? ? ? ? ? Collections.sort(locks);

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

? ? ? ? ? ? ? ? //如果是最小的節(jié)點(diǎn),則表示取得鎖

? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? }

? ? ? ? ? ? //如果不是最小的節(jié)點(diǎn),找到比自己小1的節(jié)點(diǎn)

? 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節(jié)點(diǎn)

? ? ? ? ? ? // 刪除/locks/10000000001節(jié)點(diǎn)

? ? ? ? ? ? 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 分布式鎖,其實(shí)需要自己不斷去嘗試獲取鎖,比較消耗性能。

zk 分布式鎖,獲取不到鎖,注冊個(gè)監(jiān)聽器即可,不需要不斷主動(dòng)嘗試獲取鎖,性能開銷較小。

另外一點(diǎn)就是,如果是 redis 獲取鎖的那個(gè)客戶端 出現(xiàn) bug 掛了,那么只能等待超時(shí)時(shí)間之后才能釋放鎖;而 zk 的話,因?yàn)閯?chuàng)建的是臨時(shí) znode,只要客戶端掛了,znode 就沒了,此時(shí)就自動(dòng)釋放鎖。

redis 分布式鎖大家沒發(fā)現(xiàn)好麻煩嗎?遍歷上鎖,計(jì)算時(shí)間等等......zk 的分布式鎖語義清晰實(shí)現(xiàn)簡單。

所以先不分析太多的東西,就說這兩點(diǎn),我個(gè)人實(shí)踐認(rèn)為 zk 的分布式鎖比 redis 的分布式鎖牢靠、而且模型簡單易用。

需要更多java架構(gòu)資料及面試真題請關(guān)注私信回復(fù) 666?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容