ZooKeeper 實現分布式鎖
1. 前言
在我們的應用中,經常會碰見多個請求去訪問同一個資源的情況。如果請求 A 拿到這個資源數據,想要對它進行修改,但是還沒有進行事務提交,此時請求 B 訪問這個資源就會拿到修改前的數據,很顯然請求 B 拿到的是歷史數據,是不正確的。
在單個服務器的應用中,我們可以使用系統(tǒng)的線程來對這個資源進行加鎖。那么在分布式環(huán)境中我們有什么方案來解決這個問題呢?答案就是使用分布式鎖。那么什么是分布式鎖?分布式鎖又是如何實現的呢?本節(jié)我們就來講解如何使用 Zookeeper 實現分布式鎖,以及它的實現原理。
2. 分布式鎖
在講解 Zookeeper 實現的分布式鎖之前,我們先來了解什么是分布式鎖,分布式鎖的實現技術,以及分布式鎖常用的類型。
2.1 分布式鎖的特點
顧名思義,分布式鎖就是實現在分布式網絡環(huán)境中的鎖。也就是說,在鎖的基礎上加上分布式的特性,我們來分析一下分布式鎖實現的必要條件:
- 在分布式環(huán)境中,多個進程對資源的訪問必須具有順序性;
- 獲取鎖和釋放鎖的過程需要高可用和高性能;
- 具有鎖失效的機制,避免死鎖;
- 非阻塞的鎖,沒有獲取到鎖直接返回獲取鎖失敗。
介紹了分布式鎖的特點,那么有哪些技術能夠實現分布式鎖呢?
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 臨時順序節(jié)點的特性,實現分布式鎖和鎖的等待隊列。
介紹了分布式鎖的實現技術,接下來我們來介紹分布式鎖常用的類型。
2.3 分布式鎖常用的類型
分布式鎖常用的類型有兩種:一種是排他鎖,一種是共享鎖。接下來我們分別介紹這兩鎖的特點。
- 排他鎖
排他鎖也叫獨占鎖,顧名思義,也就是對資源進行獨占。排他鎖只允許獲取了該鎖的線程,對具有排他鎖的資源進行訪問,無論是寫操作還是讀操作,直到該線程主動釋放掉排他鎖。
- 共享鎖
共享鎖也就是把資源進行共享,當然共享的只有讀操作。共享鎖只對寫操作進行加鎖,其它線程的讀操作不做加鎖操作,這樣的共享機制提高了對資源訪問的性能。
介紹完分布式鎖的常用類型,接下來我們開始學習如何使用 Zookeeper 實現分布式鎖。
3. Zookeeper 實現分布式鎖
上面我們提到,Zookeeper 是根據它的臨時順序節(jié)點來實現的分布式鎖,這里我們來回顧一下臨時順序節(jié)點的特性。
3.1 臨時順序節(jié)點
臨時順序節(jié)點:
- 節(jié)點具有臨時性,創(chuàng)建該節(jié)點的 Zookeeper 客戶端與 Zookeeper 服務端斷開連接時,該節(jié)點會自動被 Zookeeper 服務端刪除;
- 節(jié)點具有順序性,創(chuàng)建該節(jié)點時,Zookeeper 服務端會根據創(chuàng)建時間的順序在該節(jié)點名稱后面加上順序編號。
回顧了臨時順序節(jié)點的特性,接下來我們就使用 Zookeeper 的 Java 客戶端 Curator 來創(chuàng)建臨時順序節(jié)點,我們可以使用在 Zookeeper Curator 一節(jié)創(chuàng)建的 Spring Boot 測試項目來進行測試。
我們可以在測試類 CuratorDemoApplicationTests 中編寫測試用例:
@SpringBootTest
class CuratorDemoApplicationTests {
@Autowired
private CuratorService curatorService;
@Test
void contextLoads() throws Exception {
// 獲取客戶端
CuratorFramework client = curatorService.getCuratorClient();
// 開啟會話
client.start();
// 第一次創(chuàng)建臨時順序節(jié)點
String s1 = client.create()
// 如果有父節(jié)點會一起創(chuàng)建
.creatingParentsIfNeeded()
// 節(jié)點類型:臨時順序節(jié)點
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
// 節(jié)點路徑 /wiki
.forPath("/wiki-");
// 輸出
System.out.println(s1);
// 第二次創(chuàng)建臨時順序節(jié)點
String s2 = client.create()
// 如果有父節(jié)點會一起創(chuàng)建
.creatingParentsIfNeeded()
// 節(jié)點類型:臨時順序節(jié)點
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
// 節(jié)點路徑 /wiki
.forPath("/wiki-");
// 輸出
System.out.println(s2);
// 關閉客戶端
client.close();
}
}
執(zhí)行測試方法,控制臺輸出:
/wiki-0000000000
/wiki-0000000001
我們可以發(fā)現,控制臺一共輸出了兩個 /wiki 節(jié)點,而且每個 /wiki 節(jié)點后面都增加了編號,此時我們去 zkCli 命令行客戶端查看所有節(jié)點,發(fā)現并沒有 /wiki 節(jié)點。因為在我們的測試程序中,我們關閉了客戶端,所以臨時節(jié)點會被移除。
Tips: 如果這里創(chuàng)建失敗,請同學們注意父節(jié)點是否存在 ACL 訪問控制。
回顧了臨時順序節(jié)點,那么如何使用 Zookeeper 的臨時順序節(jié)點來實現分布式鎖呢?接下來我們就開始介紹如何使用 Zookeeper 的臨時順序節(jié)點來控制它們的訪問順序。
3.2 分布式鎖實現
本節(jié)我們來介紹分布式鎖實現的具體步驟:
-
創(chuàng)建臨時順序節(jié)點: 每一次獲取資源的請求,我們都需要使用 Zookeeper 客戶端創(chuàng)建一個臨時順序節(jié)點,用這個臨時順序節(jié)點在 Zookeeper 服務端中獲取鎖。
-
獲取鎖: 這里的鎖并不具體指代什么,而是根據 Zookeeper 的臨時順序節(jié)點的順序來決定是否獲取了鎖。如果該節(jié)點的順序編號是最小的,則說明該節(jié)點是排在最前面的,在它之前無人占領資源,也就可以說該節(jié)點獲取了鎖,具有訪問資源的權限。
- 監(jiān)聽鎖: 如果獲取鎖這一步發(fā)現 Zookeeper 客戶端創(chuàng)建的臨時順序節(jié)點的順序編號不是最小的,也就是在這個臨時順序節(jié)點之前存在其它臨時順序節(jié)點,那么就可以說這個節(jié)點獲取鎖失敗了,它會進入等待隊列。我們可以監(jiān)聽它的前一個節(jié)點,只要它的前一個臨時順序節(jié)點的刪除事件觸發(fā),我們就可以獲取臨時順序節(jié)點的列表來重新確認這個節(jié)點的順序。
- 釋放鎖: 當一個請求對資源的操作結束后,我們可以使用 Zookeeper 客戶端的節(jié)點刪除 API 來刪除這個請求創(chuàng)建的臨時順序節(jié)點。除了使用 API 來主動釋放鎖之外,根據臨時順序節(jié)點的特性,當創(chuàng)建這個臨時順序節(jié)點的 Zookeeper 客戶端與 Zookeeper 服務端斷開連接時,這個臨時順序節(jié)點會被 Zookeeper 服務端移除。這兩種方式都會觸發(fā)臨時節(jié)點的刪除事件,讓下一個臨時順序節(jié)點來確認自身的順序。
4. 總結
本節(jié)內容中,我們學習了什么是分布式鎖,以及它的特點和類型,還學習了使用 Zookeeper 實現分布式鎖的主要步驟。以下是本節(jié)內容的總結:
- 分布式鎖的特點和常用類型。
- 臨時順序節(jié)點的特性。
- 使用 Zookeeper 實現分布式鎖的主要步驟。