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 客戶端的節點刪除 API 來刪除這個請求建立的臨時順序節點。除了使用 API 來主動釋放鎖之外,根據臨時順序節點的特性,當建立這個臨時順序節點的 Zookeeper 客戶端與 Zookeeper 服務端斷開連線時,這個臨時順序節點會被 Zookeeper 服務端移除。這兩種方式都會觸發臨時節點的刪除事件,讓下一個臨時順序節點來確認自身的順序。
4。 總結
本節內容中,我們學習了什麼是分散式鎖,以及它的特點和型別,還學習了使用 Zookeeper 實現分散式鎖的主要步驟。以下是本節內容的總結:
分散式鎖的特點和常用型別。
臨時順序節點的特性。
使用 Zookeeper 實現分散式鎖的主要步驟。