當(dāng)前位置:首頁 > 公眾號精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]目前互聯(lián)網(wǎng)項目越來越多的項目采用集群部署,也就是分布式情況,這兩種鎖就有些不夠用了。

作者:whynot_0

編輯:陶家龍

出處:juejin.im/post/6891571079702118407


不過目前互聯(lián)網(wǎng)項目越來越多的項目采用集群部署,也就是分布式情況,這兩種鎖就有些不夠用了。


來兩張圖舉例說明下,本地鎖的情況下:

分布式鎖用Redis好,還是Zookeeper好?

分布式鎖情況下:

分布式鎖用Redis好,還是Zookeeper好?

就其思想來說,就是一種“我全都要”的思想,所有服務(wù)都到一個統(tǒng)一的地方來取鎖,只有取到鎖的才能繼續(xù)執(zhí)行下去。 分布式鎖用Redis好,還是Zookeeper好?

說完思想,下面來說一下具體的實(shí)現(xiàn)。


Redis 實(shí)現(xiàn)


為實(shí)現(xiàn)分布式鎖,在 Redis 中存在 SETNX key value 命令,意為 set if not exists(如果不存在該 key,才去 set 值),就比如說是張三去上廁所,看廁所門鎖著,他就不進(jìn)去了,廁所門開著他才去。 分布式鎖用Redis好,還是Zookeeper好?

可以看到,第一次 set 返回了 1,表示成功,但是第二次返回 0,表示 set 失敗,因?yàn)橐呀?jīng)存在這個 key 了。


當(dāng)然只靠 setnx 這個命令可以嗎?當(dāng)然是不行的,試想一種情況,張三在廁所里,但他在里面一直沒有釋放,一直在里面蹲著,那外面人想去廁所全部都去不了,都想錘死他了。


Redis 同理,假設(shè)已經(jīng)進(jìn)行了加鎖,但是因?yàn)殄礄C(jī)或者出現(xiàn)異常未釋放鎖,就造成了所謂的“死鎖”。 分布式鎖用Redis好,還是Zookeeper好?

聰明的你們肯定早都想到了,為它設(shè)置過期時間不就好了,可以 SETEX key seconds value 命令,為指定 key 設(shè)置過期時間,單位為秒。


但這樣又有另一個問題,我剛加鎖成功,還沒設(shè)置過期時間,Redis 宕機(jī)了不就又死鎖了,所以說要保證原子性吖,要么一起成功,要么一起失敗。


當(dāng)然我們能想到的 Redis 肯定早都為你實(shí)現(xiàn)好了,在 Redis 2.8 的版本后,Redis 就為我們提供了一條組合命令 SET key value ex seconds nx,加鎖的同時設(shè)置過期時間。 分布式鎖用Redis好,還是Zookeeper好?

就好比是公司規(guī)定每人最多只能在廁所呆 2 分鐘,不管釋放沒釋放完都得出來,這樣就解決了“死鎖”問題。


但這樣就沒有問題了嗎?怎么可能。


試想又一種情況,廁所門肯定只能從里面開啊,張三上完廁所后張四進(jìn)去鎖上門,但是外面人以為還是張三在里面,而且已經(jīng)過了 3 分鐘了,就直接把門給撬開了,一看里面卻是張四,這就很尷尬啊。


換成 Redis 就是說比如一個業(yè)務(wù)執(zhí)行時間很長,鎖已經(jīng)自己過期了,別人已經(jīng)設(shè)置了新的鎖,但是當(dāng)業(yè)務(wù)執(zhí)行完之后直接釋放鎖,就有可能是刪除了別人加的鎖,這不是亂套了嗎。


所以在加鎖時候,要設(shè)一個隨機(jī)值,在刪除鎖時進(jìn)行比對,如果是自己的鎖,才刪除。


多說無益,煩人,直接上代碼:
//基于jedis和lua腳本來實(shí)現(xiàn) privatestaticfinal String LOCK_SUCCESS = "OK";
privatestaticfinal Long RELEASE_SUCCESS = 1L;
privatestaticfinal String SET_IF_NOT_EXIST = "NX";
privatestaticfinal String SET_WITH_EXPIRE_TIME = "PX"; @Override public String acquire() { try { // 獲取鎖的超時時間,超過這個時間則放棄獲取鎖 long end = System.currentTimeMillis() + acquireTimeout; // 隨機(jī)生成一個 value String requireToken = UUID.randomUUID().toString(); while (System.currentTimeMillis() < end) { String result = jedis
                .set(lockKey, requireToken, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return requireToken;
            } try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    } catch (Exception e) {
        log.error("acquire lock due to error", e);
    }

    returnnull;
} @Override public boolean release(String identify) { if (identify == null) {
        returnfalse;
    } //通過lua腳本進(jìn)行比對刪除操作,保證原子性 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = new Object(); try {
        result = jedis.eval(script, Collections.singletonList(lockKey),
            Collections.singletonList(identify)); if (RELEASE_SUCCESS.equals(result)) {
            log.info("release lock success, requestToken:{}", identify);
            returntrue;
        }
    } catch (Exception e) {
        log.error("release lock due to error", e);
    } finally { if (jedis != null) {
            jedis.close();
        }
    }

    log.info("release lock failed, requestToken:{}, result:{}", identify, result);
    returnfalse;
}

思考:加鎖和釋放鎖的原子性可以用 lua 腳本來保證,那鎖的自動續(xù)期改如何實(shí)現(xiàn)呢?


Redisson 實(shí)現(xiàn)


Redisson 顧名思義,Redis 的兒子,本質(zhì)上還是 Redis 加鎖,不過是對 Redis 做了很多封裝,它不僅提供了一系列的分布式的 Java 常用對象,還提供了許多分布式服務(wù)。


在引入 Redisson 的依賴后,就可以直接進(jìn)行調(diào)用:
<dependency> <groupId>org.redissongroupId> <artifactId>redissonartifactId> <version>3.13.4version> dependency> 

先來一段 Redisson 的加鎖代碼:
private void test() { //分布式鎖名  鎖的粒度越細(xì),性能越好 RLock lock = redissonClient.getLock("test_lock"); lock.lock(); try { //具體業(yè)務(wù)...... } finally { lock.unlock();
    }
}

就是這么簡單,使用方法 jdk 的 ReentrantLock 差不多,并且也支持 ReadWriteLock(讀寫鎖)、Reentrant Lock(可重入鎖)、Fair Lock(公平鎖)、RedLock(紅鎖)等各種鎖,詳細(xì)可以參照redisson官方文檔來查看。

分布式鎖用Redis好,還是Zookeeper好?

那么 Redisson 到底有哪些優(yōu)勢呢?鎖的自動續(xù)期(默認(rèn)都是 30 秒),如果業(yè)務(wù)超長,運(yùn)行期間會自動給鎖續(xù)上新的 30s,不用擔(dān)心業(yè)務(wù)執(zhí)行時間超長而鎖被自動刪掉。


加鎖的業(yè)務(wù)只要運(yùn)行完成,就不會給當(dāng)前續(xù)期,即便不手動解鎖,鎖默認(rèn)在 30s 后刪除,不會造成死鎖問題。


前面也提到了鎖的自動續(xù)期,我們來看看 Redisson 是如何來實(shí)現(xiàn)的。


先說明一下,這里主要講的是 Redisson 中的 RLock,也就是可重入鎖,有兩種實(shí)現(xiàn)方法:

// 最常見的使用方法 lock.lock(); // 加鎖以后10秒鐘自動解鎖 // 無需調(diào)用unlock方法手動解鎖 lock.lock(10, TimeUnit.SECONDS);

而只有無參的方法是提供鎖的自動續(xù)期操作的,內(nèi)部使用的是“看門狗”機(jī)制,我們來看一看源碼。

分布式鎖用Redis好,還是Zookeeper好?

分布式鎖用Redis好,還是Zookeeper好?

不管是空參還是帶參方法,它們都調(diào)用的是同一個 lock 方法,未傳參的話時間傳了一個 -1,而帶參的方法傳過去的就是實(shí)際傳入的時間。

分布式鎖用Redis好,還是Zookeeper好? 繼續(xù)點(diǎn)進(jìn) scheduleExpirationRenewal 方法: 分布式鎖用Redis好,還是Zookeeper好? 點(diǎn)進(jìn) renewExpiration 方法: 分布式鎖用Redis好,還是Zookeeper好?

總結(jié)一下,就是當(dāng)我們指定鎖過期時間,那么鎖到時間就會自動釋放。如果沒有指定鎖過期時間,就使用看門狗的默認(rèn)時間 30s,只要占鎖成功,就會啟動一個定時任務(wù),每隔 10s 給鎖設(shè)置新的過期時間,時間為看門狗的默認(rèn)時間,直到鎖釋放。


小結(jié):雖然 lock() 有自動續(xù)鎖機(jī)制,但是開發(fā)中還是推薦使用 lock(time,timeUnit),因?yàn)樗〉袅苏麄€續(xù)期帶來的性能損,可以設(shè)置過期時間長一點(diǎn),搭配 unlock()。


若業(yè)務(wù)執(zhí)行完成,會手動釋放鎖,若是業(yè)務(wù)執(zhí)行超時,那一般我們服務(wù)也都會設(shè)置業(yè)務(wù)超時時間,就直接報錯了,報錯后就會通過設(shè)置的過期時間來釋放鎖。

public void test() {
    RLock lock = redissonClient.getLock("test_lock"); lock.lock(30, TimeUnit.SECONDS); try { //.......具體業(yè)務(wù) } finally { //手動釋放鎖 lock.unlock();
    }
}


基于 Zookeeper 來實(shí)現(xiàn)分布式鎖


很多小伙伴都知道在分布式系統(tǒng)中,可以用 ZK 來做注冊中心,但其實(shí)在除了做祖冊中心以外,用 ZK 來做分布式鎖也是很常見的一種方案。


先來看一下 ZK 中是如何創(chuàng)建一個節(jié)點(diǎn)的?ZK 中存在 create [-s] [-e]  path [data] 命令,-s 為創(chuàng)建有序節(jié)點(diǎn),-e 創(chuàng)建臨時節(jié)點(diǎn)。 分布式鎖用Redis好,還是Zookeeper好?

這樣就創(chuàng)建了一個父節(jié)點(diǎn)并為父節(jié)點(diǎn)創(chuàng)建了一個子節(jié)點(diǎn),組合命令意為創(chuàng)建一個臨時的有序節(jié)點(diǎn)。


而 ZK 中分布式鎖主要就是靠創(chuàng)建臨時的順序節(jié)點(diǎn)來實(shí)現(xiàn)的。至于為什么要用順序節(jié)點(diǎn)和為什么用臨時節(jié)點(diǎn)不用持久節(jié)點(diǎn)?先考慮一下,下文將作出說明。


同時還有 ZK 中如何查看節(jié)點(diǎn)?ZK 中 ls [-w] path 為查看節(jié)點(diǎn)命令,-w 為添加一個 watch(監(jiān)視器),/ 為查看根節(jié)點(diǎn)所有節(jié)點(diǎn),可以看到我們剛才所創(chuàng)建的節(jié)點(diǎn),同時如果是跟著指定節(jié)點(diǎn)名字的話為查看指定節(jié)點(diǎn)下的子節(jié)點(diǎn)。 分布式鎖用Redis好,還是Zookeeper好? 后面的 00000000 為 ZK 為順序節(jié)點(diǎn)增加的順序。注冊監(jiān)聽器也是 ZK 實(shí)現(xiàn)分布式鎖中比較重要的一個東西。
分布式鎖用Redis好,還是Zookeeper好?

下面來看一下 ZK 實(shí)現(xiàn)分布式鎖的主要流程:

  • 當(dāng)?shù)谝粋€線程進(jìn)來時會去父節(jié)點(diǎn)上創(chuàng)建一個臨時的順序節(jié)點(diǎn)。

  • 第二個線程進(jìn)來發(fā)現(xiàn)鎖已經(jīng)被持有了,就會為當(dāng)前持有鎖的節(jié)點(diǎn)注冊一個 watcher 監(jiān)聽器。

  • 第三個線程進(jìn)來發(fā)現(xiàn)鎖已經(jīng)被持有了,因?yàn)槭琼樞蚬?jié)點(diǎn)的緣故,就會為上一個節(jié)點(diǎn)去創(chuàng)建一個 watcher 監(jiān)聽器。

  • 當(dāng)?shù)谝粋€線程釋放鎖后,刪除節(jié)點(diǎn),由它的下一個節(jié)點(diǎn)去占有鎖。


看到這里,聰明的小伙伴們都已經(jīng)看出來順序節(jié)點(diǎn)的好處了。非順序節(jié)點(diǎn)的話,每進(jìn)來一個線程進(jìn)來都會去持有鎖的節(jié)點(diǎn)上注冊一個監(jiān)聽器,容易引發(fā)“羊群效應(yīng)”。 分布式鎖用Redis好,還是Zookeeper好?

這么大一群羊一起向你飛奔而來,不管你頂不頂?shù)米?,反?ZK 服務(wù)器是會增大宕機(jī)的風(fēng)險。


而順序節(jié)點(diǎn)的話就不會,順序節(jié)點(diǎn)當(dāng)發(fā)現(xiàn)已經(jīng)有線程持有鎖后,會向它的上一個節(jié)點(diǎn)注冊一個監(jiān)聽器,這樣當(dāng)持有鎖的節(jié)點(diǎn)釋放后,也只有持有鎖的下一個節(jié)點(diǎn)可以搶到鎖,相當(dāng)于是排好隊來執(zhí)行的,降低服務(wù)器宕機(jī)風(fēng)險。


至于為什么使用臨時節(jié)點(diǎn),和 Redis 的過期時間一個道理,就算 ZK 服務(wù)器宕機(jī),臨時節(jié)點(diǎn)會隨著服務(wù)器的宕機(jī)而消失,避免了死鎖的情況。


下面來上一段代碼的實(shí)現(xiàn):
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.189.131:2181,192.168.189.132: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代表了一個商品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 Listlocks = 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 {
            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);
        }
    }
}

總結(jié)


既然明白了 Redis 和 ZK 分別對分布式鎖的實(shí)現(xiàn),那么總該有所不同的吧。沒錯,我都幫大家整理好了:

  • 實(shí)現(xiàn)方式的不同,Redis 實(shí)現(xiàn)為去插入一條占位數(shù)據(jù),而 ZK 實(shí)現(xiàn)為去注冊一個臨時節(jié)點(diǎn)。

  • 遇到宕機(jī)情況時,Redis 需要等到過期時間到了后自動釋放鎖,而 ZK 因?yàn)槭桥R時節(jié)點(diǎn),在宕機(jī)時候已經(jīng)是刪除了節(jié)點(diǎn)去釋放鎖。

  • Redis 在沒搶占到鎖的情況下一般會去自旋獲取鎖,比較浪費(fèi)性能,而 ZK 是通過注冊監(jiān)聽器的方式獲取鎖,性能而言優(yōu)于 Redis。


不過具體要采用哪種實(shí)現(xiàn)方式,還是需要具體情況具體分析,結(jié)合項目引用的技術(shù)棧來落地實(shí)現(xiàn)。


免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團(tuán))股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉