Redis分佈式鎖

概述

為了防止分佈式系統中的多個進程之間相互干擾,我們需要一種分佈式協調技術來對這些進程進行調度。而這個分佈式協調技術的核心就是來實現這個分佈式鎖。

為什麼要使用分佈式鎖

  • 成員變量 A 存在 JVM1、JVM2、JVM3 三個 JVM 內存中
  • 成員變量 A 同時都會在 JVM 分配一塊內存,三個請求發過來同時對這個變量操作,顯然結果是不對的
  • 不是同時發過來,三個請求分別操作三個不同 JVM 內存區域的數據,變量 A 之間不存在共享,也不具有可見性,處理的結果也是不對的
    註:該成員變量 A 是一個有狀態的對象

如果我們業務中確實存在這個場景的話,我們就需要一種方法解決這個問題,這就是分佈式鎖要解決的問題

分佈式鎖應該具備哪些條件

  • 在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行
  • 高可用的獲取鎖與釋放鎖
  • 高性能的獲取鎖與釋放鎖
  • 具備可重入特性(可理解為重新進入,由多於一個任務並發使用,而不必擔心數據錯誤)
  • 具備鎖失效機制,防止死鎖
  • 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗

分佈式鎖的實現有哪些

  • Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情況下,才能 add 成功,也就意味着線程得到了鎖。
  • Redis:和 Memcached 的方式類似,利用 Redis 的 setnx 命令。此命令同樣是原子性操作,只有在 key 不存在的情況下,才能 set 成功。
  • Zookeeper:利用 Zookeeper 的順序臨時節點,來實現分佈式鎖和等待隊列。Zookeeper 設計的初衷,就是為了實現分佈式鎖服務的。
  • Chubby:Google 公司實現的粗粒度分佈式鎖服務,底層利用了 Paxos 一致性算法。

分佈式鎖的Redis實現

加鎖:

String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)、
解鎖: if(threadId .equals(redisClient.get(key))){ del(key) }

但是,這樣做又隱含了一個新的問題,判斷和釋放鎖是兩個獨立操作,不是原子性。

出現並發的可能性

還是剛才第二點所描述的場景,雖然我們避免了線程 A 誤刪掉 key 的情況,但是同一時間有 A,B 兩個線程在訪問代碼塊,仍然是不完美的。怎麼辦呢?我們可以讓獲得鎖的線程開啟一個守護線程,用來給快要過期的鎖「續航」。

  

當過去了 29 秒,線程 A 還沒執行完,這時候守護線程會執行 expire 指令,為這把鎖「續命」20 秒。守護線程從第 29 秒開始執行,每 20 秒執行一次。

 
 

當線程 A 執行完任務,會顯式關掉守護線程。

  

另一種情況,如果節點 1 忽然斷電,由於線程 A 和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。

  

正確實現寫法如下:

 /**
     * 嘗試獲取分佈式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標識
     * @param expireTime 超期時間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 釋放分佈式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標識
     * @return 是否釋放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

對比

數據庫分佈式鎖實現

缺點:1.db操作性能較差,並且有鎖表的風險
2.非阻塞操作失敗後,需要輪詢,佔用cpu資源;
3.長時間不commit或者長時間輪詢,可能會佔用較多連接資源

Redis(緩存)分佈式鎖實現

缺點:1.鎖刪除失敗 過期時間不好控制
2.非阻塞,操作失敗後,需要輪詢,佔用cpu資源;

ZK分佈式鎖實現

缺點:性能不如redis實現,主要原因是寫操作(獲取鎖釋放鎖)都需要在Leader上執行,然後同步到follower。

總之:ZooKeeper有較好的性能和可靠性。

從理解的難易程度角度(從低到高)數據庫 > 緩存 > Zookeeper
從實現的複雜性角度(從低到高)Zookeeper >= 緩存 > 數據庫
從性能角度(從高到低)緩存 > Zookeeper >= 數據庫
從可靠性角度(從高到低)Zookeeper > 緩存 > 數據庫

Tags: