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

        共 18485字,需瀏覽 37分鐘

         ·

        2021-12-01 01:20

        程序員的成長(zhǎng)之路
        互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享 
        關(guān)注


        閱讀本文大概需要 8.5 分鐘。

        來(lái)自:juejin.im/post/6891571079702118407

        不過(guò)目前互聯(lián)網(wǎng)項(xiàng)目越來(lái)越多的項(xiàng)目采用集群部署,也就是分布式情況,這兩種鎖就有些不夠用了。
        來(lái)兩張圖舉例說(shuō)明下,本地鎖的情況下:
        分布式鎖情況下:
        就其思想來(lái)說(shuō),就是一種“我全都要”的思想,所有服務(wù)都到一個(gè)統(tǒng)一的地方來(lái)取鎖,只有取到鎖的才能繼續(xù)執(zhí)行下去。
        說(shuō)完思想,下面來(lái)說(shuō)一下具體的實(shí)現(xiàn)。

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

        為實(shí)現(xiàn)分布式鎖,在 Redis 中存在 SETNX key value 命令,意為 set if not exists(如果不存在該 key,才去 set 值),就比如說(shuō)是張三去上廁所,看廁所門鎖著,他就不進(jìn)去了,廁所門開(kāi)著他才去。
        可以看到,第一次 set 返回了 1,表示成功,但是第二次返回 0,表示 set 失敗,因?yàn)橐呀?jīng)存在這個(gè) key 了。
        當(dāng)然只靠 setnx 這個(gè)命令可以嗎?當(dāng)然是不行的,試想一種情況,張三在廁所里,但他在里面一直沒(méi)有釋放,一直在里面蹲著,那外面人想去廁所全部都去不了,都想錘死他了。
        Redis 同理,假設(shè)已經(jīng)進(jìn)行了加鎖,但是因?yàn)殄礄C(jī)或者出現(xiàn)異常未釋放鎖,就造成了所謂的“死鎖”。
        聰明的你們肯定早都想到了,為它設(shè)置過(guò)期時(shí)間不就好了,可以 SETEX key seconds value 命令,為指定 key 設(shè)置過(guò)期時(shí)間,單位為秒。
        但這樣又有另一個(gè)問(wèn)題,我剛加鎖成功,還沒(méi)設(shè)置過(guò)期時(shí)間,Redis 宕機(jī)了不就又死鎖了,所以說(shuō)要保證原子性吖,要么一起成功,要么一起失敗。
        當(dāng)然我們能想到的 Redis 肯定早都為你實(shí)現(xiàn)好了,在 Redis 2.8 的版本后,Redis 就為我們提供了一條組合命令 SET key value ex seconds nx,加鎖的同時(shí)設(shè)置過(guò)期時(shí)間。
        就好比是公司規(guī)定每人最多只能在廁所呆 2 分鐘,不管釋放沒(méi)釋放完都得出來(lái),這樣就解決了“死鎖”問(wèn)題。
        但這樣就沒(méi)有問(wèn)題了嗎?怎么可能。
        試想又一種情況,廁所門肯定只能從里面開(kāi)啊,張三上完廁所后張四進(jìn)去鎖上門,但是外面人以為還是張三在里面,而且已經(jīng)過(guò)了 3 分鐘了,就直接把門給撬開(kāi)了,一看里面卻是張四,這就很尷尬啊。
        換成 Redis 就是說(shuō)比如一個(gè)業(yè)務(wù)執(zhí)行時(shí)間很長(zhǎng),鎖已經(jīng)自己過(guò)期了,別人已經(jīng)設(shè)置了新的鎖,但是當(dāng)業(yè)務(wù)執(zhí)行完之后直接釋放鎖,就有可能是刪除了別人加的鎖,這不是亂套了嗎。
        所以在加鎖時(shí)候,要設(shè)一個(gè)隨機(jī)值,在刪除鎖時(shí)進(jìn)行比對(duì),如果是自己的鎖,才刪除。
        多說(shuō)無(wú)益,煩人,直接上代碼:
        //基于jedis和lua腳本來(lái)實(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 {
                // 獲取鎖的超時(shí)時(shí)間,超過(guò)這個(gè)時(shí)間則放棄獲取鎖
                long end = System.currentTimeMillis() + acquireTimeout;
                // 隨機(jī)生成一個(gè) 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;
            }
            //通過(guò)lua腳本進(jìn)行比對(duì)刪除操作,保證原子性
            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 腳本來(lái)保證,那鎖的自動(dòng)續(xù)期改如何實(shí)現(xiàn)呢?
        Redisson 實(shí)現(xiàn)
        Redisson 顧名思義,Redis 的兒子,本質(zhì)上還是 Redis 加鎖,不過(guò)是對(duì) Redis 做了很多封裝,它不僅提供了一系列的分布式的 Java 常用對(duì)象,還提供了許多分布式服務(wù)。
        在引入 Redisson 的依賴后,就可以直接進(jìn)行調(diào)用:
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
        先來(lái)一段 Redisson 的加鎖代碼:
        private void test() {
            //分布式鎖名  鎖的粒度越細(xì),性能越好
            RLock lock = redissonClient.getLock("test_lock");
            lock.lock();
            try {
                //具體業(yè)務(wù)......
            } finally {
                lock.unlock();
            }
        }
        就是這么簡(jiǎn)單,使用方法 jdk 的 ReentrantLock 差不多,并且也支持 ReadWriteLock(讀寫鎖)、Reentrant Lock(可重入鎖)、Fair Lock(公平鎖)、RedLock(紅鎖)等各種鎖,詳細(xì)可以參照redisson官方文檔來(lái)查看。
        那么 Redisson 到底有哪些優(yōu)勢(shì)呢?鎖的自動(dòng)續(xù)期(默認(rèn)都是 30 秒),如果業(yè)務(wù)超長(zhǎng),運(yùn)行期間會(huì)自動(dòng)給鎖續(xù)上新的 30s,不用擔(dān)心業(yè)務(wù)執(zhí)行時(shí)間超長(zhǎng)而鎖被自動(dòng)刪掉。
        加鎖的業(yè)務(wù)只要運(yùn)行完成,就不會(huì)給當(dāng)前續(xù)期,即便不手動(dòng)解鎖,鎖默認(rèn)在 30s 后刪除,不會(huì)造成死鎖問(wèn)題。
        前面也提到了鎖的自動(dòng)續(xù)期,我們來(lái)看看 Redisson 是如何來(lái)實(shí)現(xiàn)的。
        先說(shuō)明一下,這里主要講的是 Redisson 中的 RLock,也就是可重入鎖,有兩種實(shí)現(xiàn)方法:
        // 最常見(jiàn)的使用方法
        lock.lock();
         
        // 加鎖以后10秒鐘自動(dòng)解鎖
        // 無(wú)需調(diào)用unlock方法手動(dòng)解鎖
        lock.lock(10, TimeUnit.SECONDS);
        而只有無(wú)參的方法是提供鎖的自動(dòng)續(xù)期操作的,內(nèi)部使用的是“看門狗”機(jī)制,我們來(lái)看一看源碼。
        不管是空參還是帶參方法,它們都調(diào)用的是同一個(gè) lock 方法,未傳參的話時(shí)間傳了一個(gè) -1,而帶參的方法傳過(guò)去的就是實(shí)際傳入的時(shí)間。
        繼續(xù)點(diǎn)進(jìn) scheduleExpirationRenewal 方法:
        點(diǎn)進(jìn) renewExpiration 方法:
        總結(jié)一下,就是當(dāng)我們指定鎖過(guò)期時(shí)間,那么鎖到時(shí)間就會(huì)自動(dòng)釋放。如果沒(méi)有指定鎖過(guò)期時(shí)間,就使用看門狗的默認(rèn)時(shí)間 30s,只要占鎖成功,就會(huì)啟動(dòng)一個(gè)定時(shí)任務(wù),每隔 10s 給鎖設(shè)置新的過(guò)期時(shí)間,時(shí)間為看門狗的默認(rèn)時(shí)間,直到鎖釋放。
        小結(jié):雖然 lock() 有自動(dòng)續(xù)鎖機(jī)制,但是開(kāi)發(fā)中還是推薦使用 lock(time,timeUnit),因?yàn)樗〉袅苏麄€(gè)續(xù)期帶來(lái)的性能損,可以設(shè)置過(guò)期時(shí)間長(zhǎng)一點(diǎn),搭配 unlock()。
        若業(yè)務(wù)執(zhí)行完成,會(huì)手動(dòng)釋放鎖,若是業(yè)務(wù)執(zhí)行超時(shí),那一般我們服務(wù)也都會(huì)設(shè)置業(yè)務(wù)超時(shí)時(shí)間,就直接報(bào)錯(cuò)了,報(bào)錯(cuò)后就會(huì)通過(guò)設(shè)置的過(guò)期時(shí)間來(lái)釋放鎖。
        public void test() {
            RLock lock = redissonClient.getLock("test_lock");
            lock.lock(30, TimeUnit.SECONDS);
            try {
                //.......具體業(yè)務(wù)
            } finally {
                //手動(dòng)釋放鎖
                lock.unlock();
            }
        }

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

        很多小伙伴都知道在分布式系統(tǒng)中,可以用 ZK 來(lái)做注冊(cè)中心,但其實(shí)在除了做祖冊(cè)中心以外,用 ZK 來(lái)做分布式鎖也是很常見(jiàn)的一種方案。
        先來(lái)看一下 ZK 中是如何創(chuàng)建一個(gè)節(jié)點(diǎn)的?ZK 中存在 create [-s] [-e] path [data] 命令,-s 為創(chuàng)建有序節(jié)點(diǎn),-e 創(chuàng)建臨時(shí)節(jié)點(diǎn)。
        這樣就創(chuàng)建了一個(gè)父節(jié)點(diǎn)并為父節(jié)點(diǎn)創(chuàng)建了一個(gè)子節(jié)點(diǎn),組合命令意為創(chuàng)建一個(gè)臨時(shí)的有序節(jié)點(diǎn)。
        而 ZK 中分布式鎖主要就是靠創(chuàng)建臨時(shí)的順序節(jié)點(diǎn)來(lái)實(shí)現(xiàn)的。至于為什么要用順序節(jié)點(diǎn)和為什么用臨時(shí)節(jié)點(diǎn)不用持久節(jié)點(diǎn)?先考慮一下,下文將作出說(shuō)明。
        同時(shí)還有 ZK 中如何查看節(jié)點(diǎn)?ZK 中 ls [-w] path 為查看節(jié)點(diǎn)命令,-w 為添加一個(gè) watch(監(jiān)視器),/ 為查看根節(jié)點(diǎn)所有節(jié)點(diǎn),可以看到我們剛才所創(chuàng)建的節(jié)點(diǎn),同時(shí)如果是跟著指定節(jié)點(diǎn)名字的話為查看指定節(jié)點(diǎn)下的子節(jié)點(diǎn)。
        后面的 00000000 為 ZK 為順序節(jié)點(diǎn)增加的順序。注冊(cè)監(jiān)聽(tīng)器也是 ZK 實(shí)現(xiàn)分布式鎖中比較重要的一個(gè)東西。
        下面來(lái)看一下 ZK 實(shí)現(xiàn)分布式鎖的主要流程:
        • 當(dāng)?shù)谝粋€(gè)線程進(jìn)來(lái)時(shí)會(huì)去父節(jié)點(diǎn)上創(chuàng)建一個(gè)臨時(shí)的順序節(jié)點(diǎn)。
        • 第二個(gè)線程進(jìn)來(lái)發(fā)現(xiàn)鎖已經(jīng)被持有了,就會(huì)為當(dāng)前持有鎖的節(jié)點(diǎn)注冊(cè)一個(gè) watcher 監(jiān)聽(tīng)器。
        • 第三個(gè)線程進(jìn)來(lái)發(fā)現(xiàn)鎖已經(jīng)被持有了,因?yàn)槭琼樞蚬?jié)點(diǎn)的緣故,就會(huì)為上一個(gè)節(jié)點(diǎn)去創(chuàng)建一個(gè) watcher 監(jiān)聽(tīng)器。
        • 當(dāng)?shù)谝粋€(gè)線程釋放鎖后,刪除節(jié)點(diǎn),由它的下一個(gè)節(jié)點(diǎn)去占有鎖。
        看到這里,聰明的小伙伴們都已經(jīng)看出來(lái)順序節(jié)點(diǎn)的好處了。非順序節(jié)點(diǎn)的話,每進(jìn)來(lái)一個(gè)線程進(jìn)來(lái)都會(huì)去持有鎖的節(jié)點(diǎn)上注冊(cè)一個(gè)監(jiān)聽(tīng)器,容易引發(fā)“羊群效應(yīng)”。
        這么大一群羊一起向你飛奔而來(lái),不管你頂不頂?shù)米。凑?ZK 服務(wù)器是會(huì)增大宕機(jī)的風(fēng)險(xiǎn)。
        而順序節(jié)點(diǎn)的話就不會(huì),順序節(jié)點(diǎn)當(dāng)發(fā)現(xiàn)已經(jīng)有線程持有鎖后,會(huì)向它的上一個(gè)節(jié)點(diǎn)注冊(cè)一個(gè)監(jiān)聽(tīng)器,這樣當(dāng)持有鎖的節(jié)點(diǎn)釋放后,也只有持有鎖的下一個(gè)節(jié)點(diǎn)可以搶到鎖,相當(dāng)于是排好隊(duì)來(lái)執(zhí)行的,降低服務(wù)器宕機(jī)風(fēng)險(xiǎn)。
        至于為什么使用臨時(shí)節(jié)點(diǎn),和 Redis 的過(guò)期時(shí)間一個(gè)道理,就算 ZK 服務(wù)器宕機(jī),臨時(shí)節(jié)點(diǎn)會(huì)隨著服務(wù)器的宕機(jī)而消失,避免了死鎖的情況。
        下面來(lái)上一段代碼的實(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代表了一個(gè)商品id,比如說(shuō)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 {
                    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 分別對(duì)分布式鎖的實(shí)現(xiàn),那么總該有所不同的吧。沒(méi)錯(cuò),我都幫大家整理好了:
        • 實(shí)現(xiàn)方式的不同,Redis 實(shí)現(xiàn)為去插入一條占位數(shù)據(jù),而 ZK 實(shí)現(xiàn)為去注冊(cè)一個(gè)臨時(shí)節(jié)點(diǎn)。

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

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

        不過(guò)具體要采用哪種實(shí)現(xiàn)方式,還是需要具體情況具體分析,結(jié)合項(xiàng)目引用的技術(shù)棧來(lái)落地實(shí)現(xiàn)。
        <END>
        推薦閱讀:

        心態(tài)崩了!稅前2萬(wàn)4,到手1萬(wàn)4,年終獎(jiǎng)扣稅方式1月1日起施行~

        面試官:為什么要合并 HTTP 請(qǐng)求?

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

        獲取方式:點(diǎn)個(gè)「在看」,點(diǎn)擊上方小卡片,進(jìn)入公眾號(hào)后回復(fù)「面試題」領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

        朕已閱 

        瀏覽 41
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        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>
            撩开裙子摸摸下面流水 | 老太婆性杂交视频 | 欧美成人视频网站导航免费 | av无码aV天天aV天天爽 | 日韩欧美国产一区二区三区 | 西施裸体被强扒内裤啪啪 | 我想看操大逼的有没有免费毛片 | 国产青青操91av | 国产精品婷婷久久久 | 女人被狂躁爽的动态gif |