1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 1671字,需瀏覽 4分鐘

         ·

        2021-12-18 14:54

        點(diǎn)擊關(guān)注公眾號,Java干貨及時送達(dá)??

        來源:juejin.im/post/6891571079702118407

        • Redis 實(shí)現(xiàn)
        • 基于 Zookeeper 來實(shí)現(xiàn)分布式鎖
        • 總結(jié)

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

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

        圖片

        分布式鎖情況下:

        圖片

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

        圖片

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

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

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

        圖片

        可以看到,第一次 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)異常未釋放鎖,就造成了所謂的“死鎖”。

        圖片

        聰明的你們肯定早都想到了,為它設(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è)置過期時間。

        圖片

        就好比是公司規(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()?????????????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)用:


        ????org.redisson
        ????redisson
        ????3.13.4

        先來一段 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官方文檔來查看。

        圖片

        那么 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ī)制,我們來看一看源碼。

        圖片
        圖片

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

        圖片

        繼續(xù)點(diǎn)進(jìn) scheduleExpirationRenewal 方法:

        圖片

        點(diǎn)進(jìn) renewExpiration 方法:

        圖片

        總結(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)。

        圖片

        這樣就創(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)。

        圖片

        后面的 00000000 為 ZK 為順序節(jié)點(diǎn)增加的順序。注冊監(jiān)聽器也是 ZK 實(shí)現(xiàn)分布式鎖中比較重要的一個東西。

        圖片

        下面來看一下 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)”。

        圖片

        這么大一群羊一起向你飛奔而來,不管你頂不頂?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
        ????????List?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?????????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)。

        1.?騷操作:不重啟 JVM,如何替換掉已經(jīng)加載的類?

        2.?監(jiān)控告警滿飛天,運(yùn)維在家睡到自然醒...

        3.?Redis緩存使用技巧和設(shè)計方案

        4.?干掉Random:這個類已經(jīng)成為獲取隨機(jī)數(shù)的王者

        最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

        獲取方式:點(diǎn)“在看”,關(guān)注公眾號并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

        文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

        謝謝支持喲 (*^__^*)

        瀏覽 57
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            久操影视 | 全黄h全肉医生短篇禁乱 | 国产精品内射婷婷一级二 | 四虎色播 | 能免费看黄的网站 | 久艹大香蕉 | 婷婷五月综合久久 | 色中文娱乐| 成人免费视频一区二区 | 淫-欲-乱-交-2-3 |