ZooKeeper 實現分散式鎖

1。 前言

在我們的應用中,經常會碰見多個請求去訪問同一個資源的情況。如果請求 A 拿到這個資源資料,想要對它進行修改,但是還沒有進行事務提交,此時請求 B 訪問這個資源就會拿到修改前的資料,很顯然請求 B 拿到的是歷史資料,是不正確的。

在單個伺服器的應用中,我們可以使用系統的執行緒來對這個資源進行加鎖。那麼在分散式環境中我們有什麼方案來解決這個問題呢?答案就是使用分散式鎖。那麼什麼是分散式鎖?分散式鎖又是如何實現的呢?本節我們就來講解如何使用 Zookeeper 實現分散式鎖,以及它的實現原理。

2。 分散式鎖

在講解 Zookeeper 實現的分散式鎖之前,我們先來了解什麼是分散式鎖,分散式鎖的實現技術,以及分散式鎖常用的型別。

2。1 分散式鎖的特點

顧名思義,分散式鎖就是實現在分散式網路環境中的鎖。也就是說,在鎖的基礎上加上分散式的特性,我們來分析一下分散式鎖實現的必要條件:

在分散式環境中,多個程序對資源的訪問必須具有順序性;

獲取鎖和釋放鎖的過程需要高可用和高效能;

具有鎖失效的機制,避免死鎖;

非阻塞的鎖,沒有獲取到鎖直接返回獲取鎖失敗。

介紹了分散式鎖的特點,那麼有哪些技術能夠實現分散式鎖呢?

2。2 分散式鎖的實現技術

Memcached:

使用

add

命令來新增

key

key

新增成功說明當前無人使用此

key

,也就是說無人使用此資源,相當於獲取鎖。再次使用

add

命令來新增相同的

key

時,此時

key

已存在就會新增失敗,說明有人已經使用了這個

key

,也就是說此資源被人佔用,相當於獲取鎖失敗;

Redis:

使用

setnx

命令來新增

key

key

新增成功說明當前無人使用此

key

,也就是說無人使用此資源,相當於獲取鎖。再次使用

setnx

命令來新增相同的

key

時,此時

key

已存在就會新增失敗,說明有人已經使用了這個

key

,也就是說此資源被人佔用,相當於獲取鎖失敗;

Chubby:

Google 使用 Paxos 一致性演算法實現的粗粒度分散式鎖;

Zookeeper:

使用 Zookeeper 臨時順序節點的特性,實現分散式鎖和鎖的等待佇列。

介紹了分散式鎖的實現技術,接下來我們來介紹分散式鎖常用的型別。

2。3 分散式鎖常用的型別

分散式鎖常用的型別有兩種:一種是排他鎖,一種是共享鎖。接下來我們分別介紹這兩鎖的特點。

排他鎖

排他鎖也叫獨佔鎖,顧名思義,也就是對資源進行獨佔。排他鎖只允許獲取了該鎖的執行緒,對具有排他鎖的資源進行訪問,無論是寫操作還是讀操作,直到該執行緒主動釋放掉排他鎖。

共享鎖

共享鎖也就是把資源進行共享,當然共享的只有讀操作。共享鎖只對寫操作進行加鎖,其它執行緒的讀操作不做加鎖操作,這樣的共享機制提高了對資源訪問的效能。

介紹完分散式鎖的常用型別,接下來我們開始學習如何使用 Zookeeper 實現分散式鎖。

3。 Zookeeper 實現分散式鎖

上面我們提到,Zookeeper 是根據它的臨時順序節點來實現的分散式鎖,這裡我們來回顧一下臨時順序節點的特性。

3。1 臨時順序節點

臨時順序節點:

節點具有臨時性,建立該節點的 Zookeeper 客戶端與 Zookeeper 服務端斷開連線時,該節點會自動被 Zookeeper 服務端刪除;

節點具有順序性,建立該節點時,Zookeeper 服務端會根據建立時間的順序在該節點名稱後面加上順序編號。

回顧了臨時順序節點的特性,接下來我們就使用 Zookeeper 的 Java 客戶端 Curator 來建立臨時順序節點,我們可以使用在 Zookeeper Curator 一節建立的 Spring Boot 測試專案來進行測試。

我們可以在測試類 CuratorDemoApplicationTests 中編寫測試用例:

@SpringBootTestclass CuratorDemoApplicationTests { @Autowired private CuratorService curatorService; @Test void contextLoads() throws Exception { // 獲取客戶端 CuratorFramework client = curatorService。getCuratorClient(); // 開啟會話 client。start(); // 第一次建立臨時順序節點 String s1 = client。create() // 如果有父節點會一起建立 。creatingParentsIfNeeded() // 節點型別:臨時順序節點 。withMode(CreateMode。EPHEMERAL_SEQUENTIAL) // 節點路徑 /wiki 。forPath(“/wiki-”); // 輸出 System。out。println(s1); // 第二次建立臨時順序節點 String s2 = client。create() // 如果有父節點會一起建立 。creatingParentsIfNeeded() // 節點型別:臨時順序節點 。withMode(CreateMode。EPHEMERAL_SEQUENTIAL) // 節點路徑 /wiki 。forPath(“/wiki-”); // 輸出 System。out。println(s2); // 關閉客戶端 client。close(); }}

執行測試方法,控制檯輸出:

/wiki-0000000000/wiki-0000000001

我們可以發現,控制檯一共輸出了兩個 /wiki 節點,而且每個 /wiki 節點後面都增加了編號,此時我們去 zkCli 命令列客戶端檢視所有節點,發現並沒有 /wiki 節點。因為在我們的測試程式中,我們關閉了客戶端,所以臨時節點會被移除。

Tips:

如果這裡建立失敗,請同學們注意父節點是否存在 ACL 訪問控制。

回顧了臨時順序節點,那麼如何使用 Zookeeper 的臨時順序節點來實現分散式鎖呢?接下來我們就開始介紹如何使用 Zookeeper 的臨時順序節點來控制它們的訪問順序。

3。2 分散式鎖實現

本節我們來介紹分散式鎖實現的具體步驟:

建立臨時順序節點:

每一次獲取資源的請求,我們都需要使用 Zookeeper 客戶端建立一個臨時順序節點,用這個臨時順序節點在 Zookeeper 服務端中獲取鎖。

獲取鎖:

這裡的鎖並不具體指代什麼,而是根據 Zookeeper 的臨時順序節點的順序來決定是否獲取了鎖。如果該節點的順序編號是最小的,則說明該節點是排在最前面的,在它之前無人佔領資源,也就可以說該節點獲取了鎖,具有訪問資源的許可權。

ZooKeeper 實現分散式鎖

監聽鎖:

如果獲取鎖這一步發現 Zookeeper 客戶端建立的臨時順序節點的順序編號不是最小的,也就是在這個臨時順序節點之前存在其它臨時順序節點,那麼就可以說這個節點獲取鎖失敗了,它會進入等待佇列。我們可以監聽它的前一個節點,只要它的前一個臨時順序節點的刪除事件觸發,我們就可以獲取臨時順序節點的列表來重新確認這個節點的順序。

ZooKeeper 實現分散式鎖

釋放鎖:

當一個請求對資源的操作結束後,我們可以使用 Zookeeper 客戶端的節點刪除 API 來刪除這個請求建立的臨時順序節點。除了使用 API 來主動釋放鎖之外,根據臨時順序節點的特性,當建立這個臨時順序節點的 Zookeeper 客戶端與 Zookeeper 服務端斷開連線時,這個臨時順序節點會被 Zookeeper 服務端移除。這兩種方式都會觸發臨時節點的刪除事件,讓下一個臨時順序節點來確認自身的順序。

4。 總結

本節內容中,我們學習了什麼是分散式鎖,以及它的特點和型別,還學習了使用 Zookeeper 實現分散式鎖的主要步驟。以下是本節內容的總結:

分散式鎖的特點和常用型別。

臨時順序節點的特性。

使用 Zookeeper 實現分散式鎖的主要步驟。