分散式鎖那點事

wind瑞_發表於2017-11-16

為什麼要使用分散式鎖

為了保證一個方法在高併發情況下的同一時間只能被同一個執行緒執行,在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLcok或synchronized)進行互斥控制。但是,隨著業務發展的需要,原單體單機部署的系統被演化成分散式系統後,由於分散式系統多執行緒、多程式並且分佈在不同機器上,這將使原單機部署情況下的併發控制鎖策略失效,為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分散式鎖要解決的問題。

分散式鎖的三種實現方式

在分析分散式鎖的三種實現方式之前,先了解一下分散式鎖應該具備哪些條件。
  • 在分散式系統環境下,一個方法在同一時間只能被一個機器的一個執行緒執行;
  • 高可用的獲取鎖與釋放鎖;
  • 高效能的獲取鎖與釋放鎖;
  • 具備可重入特性;
  • 具備鎖失效機制,防止死鎖;
  • 具備阻塞鎖特性,即沒有獲取到鎖將繼續等待獲取鎖;
  • 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。

基於資料庫的實現方式

在資料庫中建立一個表,表中包含方法名等欄位,並在方法名欄位上建立唯一索引,想要執行某個方法,就使用這個方法名向表中插入資料,成功插入則獲取鎖,執行完成後刪除對應的行資料釋放鎖。

這種實現方式很簡單,但是對於分散式鎖應該具備的條件來說,它有一些問題需要解決及優化。

  • 因為是基於資料庫實現的,資料庫的可用性和效能將直接影響分散式鎖的可用性及效能,所以,資料庫需要雙機部署、資料同步、主備切換;
  • 不具備可重入的特性,因為同一個執行緒在釋放鎖之前,行資料一直存在,無法再次成功插入資料,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和執行緒資訊,在再次獲取鎖的時候,先查詢表中機器和執行緒資訊是否和當前機器和執行緒相同,若相同則直接獲取鎖;
  • 沒有鎖失效機制,因為有可能出現成功插入資料後,伺服器當機了,對應的資料沒有被刪除,當服務恢復後一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的資料;
  • 不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,迴圈多次去獲取。
優點:藉助資料庫,方案簡單。
缺點:在實際實施的過程中會遇到各種不同的問題,為了解決這些問題,實現方式將會越來越複雜;依賴資料庫需要一定的資源開銷,效能問題需要考慮。

基於Redis的實現方式

在Redis2.6.12版本之前,使用setnx命令設定key-value、使用expire命令設定key的過期時間獲取分散式鎖,使用del命令釋放分散式鎖,但是這種實現有如下一些問題:
  • setnx命令設定完key-value後,還沒來得及使用expire命令設定過期時間,當前執行緒掛掉了,會導致當前執行緒設定的key一直有效,後續執行緒無法正常通過setnx獲取鎖,造成死鎖;
  • 在分散式環境下,執行緒A通過這種實現方式獲取到了鎖,但是在獲取到鎖之後,執行被阻塞了,導致該鎖失效,此時執行緒B獲取到該鎖,之後執行緒A恢復執行,執行完成後釋放該鎖,直接使用del命令,將會把執行緒B的鎖也釋放掉,而此時執行緒B還沒執行完,將會導致不可預知的問題;
  • 為了實現高可用,將會選擇主從複製機制,但是主從複製機制是非同步的,會出現資料不同步的問題,可能導致多個機器的多個執行緒獲取到同一個鎖。
針對上面這些問題,有如下一些解決方案:
  • 第一個問題是因為兩個命令是分開執行並且不具備原子特性,如果能將這兩個命令合二為一就可以解決問題了。在Redis2.6.12版本中實現了這個功能,Redis為set命令增加了一系列選項,可以通過SET resource_name my_random_value NX PX max-lock-time來獲取分散式鎖,這個命令僅在不存在key(resource_name)的時候才能被執行成功(NX選項),並且這個key有一個max-lock-time秒的自動失效時間(PX屬性)。這個key的值是“my_random_value”,它是一個隨機值,這個值在所有的機器中必須是唯一的,用於安全釋放鎖。
  • 為了解決第二個問題,用到了“my_random_value”,釋放鎖的時候,只有key存在並且儲存的“my_random_value”值和指定的值一樣才執行del命令,此過程可以通過以下Lua指令碼實現:

    if redis.call("get",KEYS[1]) == ARGV[1] then    
        return redis.call("del",KEYS[1])
    else    
        return 0
    end複製程式碼
  • 第三個問題是因為採用了主從複製導致的,解決方案是不採用主從複製,使用RedLock演算法,這裡引用網上一段關於RedLock演算法的描述。
在Redis的分散式環境中,假設有5個Redis master,這些節點完全互相獨立,不存在主從複製或者其他叢集協調機制。為了取到鎖,客戶端應該執行以下操作:
  1. 獲取當前Unix時間,以毫秒為單位;
  2. 依次嘗試從N個例項,使用相同的key和隨機值獲取鎖。在步驟2,當向Redis設定鎖時,客戶端應該設定一個網路連線和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免伺服器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果伺服器端沒有在規定時間內響應,客戶端應該儘快嘗試另外一個Redis例項;
  3. 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(這裡是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功。
  4. 如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果);
  5. 如果因為某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis例項取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis例項上進行解鎖(即便某些Redis例項根本就沒有加鎖成功)。


通過上面的解決方案可以實現一個高效、高可用的分散式鎖,這裡推薦一個成熟、開源的分散式鎖實現,即Redisson。
優點:高效能,藉助Redis實現比較方便。
缺點:執行緒獲取鎖後,如果處理時間過長會導致鎖超時失效,所以,通過鎖超時機制不是十分可靠。

基於ZooKeeper的實現方式

ZooKeeper是一個為分散式應用提供一致性服務的開源元件,它內部是一個分層的檔案系統目錄樹結構,規定同一個目錄下只能有一個唯一檔名。基於ZooKeeper實現分散式鎖的步驟如下:
  1. 建立一個目錄mylock;
  2. 執行緒A想獲取鎖就在mylock目錄下建立臨時順序節點;
  3. 獲取mylock目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前執行緒順序號最小,獲得鎖;
  4. 執行緒B獲取所有節點,判斷自己不是最小節點,設定監聽比自己次小的節點;
  5. 執行緒A處理完,刪除自己的節點,執行緒B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。
這裡推薦一個apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分散式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。
優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:因為需要頻繁的建立和刪除節點,效能上不如Redis方式。

總結

上面的三種實現方式,沒有在所有場合都是完美的,所以,應根據不同的應用場景選擇最適合的實現方式。


相關文章