分布式利器Zookeeper(二):分布式鎖

《分布式利器Zookeeper(一)》中對ZK進行了初步的介紹以及搭建ZK集群環境,本篇博客將涉及的話題是:基于原生API方式操作ZK,Watch機制,分布式鎖思路探討等。

原生API操作ZK?

什么叫原生API操作ZK呢?實際上,利用zookeeper.jar這樣的就是基于原生的API方式操作ZK,因為這個原生API使用起來并不是讓人很舒服,于是出現了zkclient這種方式,以至到后來基于Curator框架,讓人使用ZK更加方便。有一句話,Guava is to JAVA what Curator is to Zookeeper。


連接ZK的方式

說明:

在初始化Zookeeper時,有多種構造方法可以選擇,有3個參數是必備的:connectionString(多個ZK SERVER之間以,分隔),sessionTimeout(就是zoo.cfg中的tickTime),Watcher(事件處理通知器)。

需要注意的是ZK的連接是異步的,因此我們需要CountDownLatch來幫助我們確保ZK初始化完成。

對于事件(WatchedEvent)而言,有狀態以及類型。

事件狀態及事件類型

下面,我們來看一看基于原生API方式的增刪改查:

create

注意,節點有2大類型,持久化節點、臨時節點。在此基礎上,又可以分為持久化順序節點(PERSISTENT_SEQUENTIAL)、臨時順序節點(EPHEMERAL_SEQUENTIAL)。

節點類型只支持byte[],也就是說我們是無法直接給一個對象給ZK,讓ZK幫助我們完成序列化操作的!


get/delete

這里需要注意的是,原生API對于ZK的操作其實是分為同步和異步2種方式的。

rc表示return code,就是返回碼,0即為正常。

path是傳入API的參數,ctx也是傳入的參數。

注意在刪除過程中,是需要版本檢查的,所以我們一般提供-1跳過版本檢查機制。


Watch機制

ZK有watch事件,是一次性觸發的。當watch監控的數據發生變化,會通知設置了該監控的client,即watcher。Zookeeper的watch是有自己的一些特性的:

一次性:請牢記,just watch one time! 因為ZK的監控是一次性的,所以每次必須設置監控。

輕量:WatchedEvent是ZK進行watch通知的最小單元,整個數據結構包含:事件狀態、事件類型、節點路徑。注意ZK只是通知client節點的數據發生了變化,而不會直接提供具體的數據內容。

客戶端串行執行機制:注意客戶端watch回調的過程是一個串行同步的過程,這為我們保證了順序,我們也應該意識到不能因一個watch的回調處理邏輯而影響了整個客戶端的watch回調。

下面我們來直接看代碼:

提供Watcher的實現


提供process方法


main

一定得注意的是,監控該節點和監控該節點的子節點是2碼子事。

比如exists(path,true)監控的就是該path節點的create/delete/setData;getChildren(path,watcher)監控的就是該path節點下的子節點的變化(子節點的創建、修改、刪除都會監控到,而且事件類型都是一樣的,想一想如何區分呢?給一個我的思路,就是我們得先有該path下的子節點的列表,然后watch觸發后,我們對比下該path下面的子節點SIZE大小及內容,就知道是增加的是哪個子節點,刪除的是哪個子節點了!)

getChildren(path,true)和getChildren(path,watcher)有什么區別?前者是沿用上下文中的Watcher,而后者則是可以設置一個新的Watcher的!(因此,要想做到一直監控,那么就有2種方式,一個是注意每次設置成true,或者干脆每次設置一個新的Watcher)

從上面的討論中,你大概能了解到原生的API其實功能上還不是很強大,有些還得我們去操心,到后面為大家介紹Curator框架,會有更好的方式進行處理。


分布式鎖思路

首先,我們不談Zookeeper是如何幫助我們處理分布式鎖的,而是先來想一想,什么是分布式鎖?為什么需要分布式鎖?有哪些場景呢?分布式鎖的使用又有哪些注意的?分布式鎖有什么特性呢?

說起鎖,我們自然想到Java為我們提供的synchronized/Lock,但是這顯然不夠,因為這只能針對一個JVM中的多個線程對共享資源的操作。那么對于多臺機器,多個進程對同一類資源進行操作的話,就是所謂分布式場景下的鎖。

各個電商平臺經常搞的“秒殺”活動需要對商品的庫存進行保護、12306火車票也不能多賣,更不允許一張票被多個人買到、這樣的場景就需要分布式鎖對共享資源進行保護!

既然,Java在分布式場景下的鎖已經無能為力,那么我們只能借助其他東西了!

我們的老朋友:DB

對,沒錯,我們能否借助DB來實現呢?要知道DB是有一些特點供我們利用的,比如DB本身就存在鎖機制(表鎖、行鎖),唯一約束等等。

假設,我們的DB中有一張表T(id,methodname,ip,threadname,......),其中id為主鍵,methodname為唯一索引。

對于多臺機器,每臺機器上的多個線程而言,對一個方法method進行操作前,先select下T表中是否存在method這條記錄,如果沒有,就插入一條記錄到T中。當然可能并發select,但是由于T表的唯一約束,使得只有一個請求能插入成功,即獲得鎖。至于釋放鎖,就是方法執行完畢后delete這條記錄即可。

考慮一些問題:如果DB掛了,怎么辦?如果由于一些因素,導致delete沒有執行成功,那么這條記錄會導致該方法再也不能被訪問!為什么要先select,為什么不直接insert呢?性能如何呢?

為了避免單點,可以主備之間實現切換;為了避免死鎖的產生,那么我們可以有一個定時任務,定期清理T表中的記錄;先select后insert,其實是為了保證鎖的可重入性,也就是說,如果一臺IP上的某個線程獲取了鎖,那么它可以不用在釋放鎖的前提下,繼續獲得鎖;性能上,如果大量的請求,將會對DB考驗,這將成為瓶頸。

到這里,還有一個明顯的問題,需要我們考慮:上述的方案,雖然保證了只會有一個請求獲得鎖,但其他請求都獲取鎖失敗返回了,而沒有進行鎖等待!當然,我們可以通過重試機制,來實現阻塞鎖,不過數據庫本身的鎖機制可以幫助我們完成。別忘了select ... for update這種阻塞式的行鎖機制,commit進行鎖的釋放。而且對于for update這種獨占鎖,如果長時間不提交釋放,會一直占用DB連接,連接爆了,就跪了!

不說了,老朋友也只能幫我們到這里了!

我們的新朋友:Redis or 其他分布式緩存(Tair/...)

既然說是緩存,相較DB,有更好的性能;既然說是分布式,當然避免了單點問題;

比如,用Redis作為分布式鎖的setnx,這里我就不細說了,總之分布式緩存需要特別注意的是緩存的失效時間。(有效時間過短,搞不好業務還沒有執行完畢,就釋放鎖了;有效時間過長,其他線程白白等待,浪費了時間,拖慢了系統處理速度)

看Zookeeper是如何幫助我們實現分布式鎖

Zookeeper中臨時順序節點的特性:

第一,節點的生命周期和client回話綁定,即創建節點的客戶端回話一旦失效,那么這個節點就會被刪除。(臨時性)

第二,每個父節點都會維護子節點創建的先后順序,自動為子節點分配一個整形數值,以后綴的形式自動追加到節點名稱中,作為這個節點最終的節點名稱。(順序性)

那么,基于臨時順序節點的特性,Zookeeper實現分布式鎖的一般思路如下:

1.client調用create()方法創建“/root/lock_”節點,注意節點類型是EPHEMERAL_SEQUENTIAL

2.client調用getChildren("/root/lock_",watch)來獲取所有已經創建的子節點,并同時在這個節點上注冊子節點變更通知的Watcher

3.客戶端獲取到所有子節點Path后,如果發現自己在步驟1中創建的節點是所有節點中最小的,那么就認為這個客戶端獲得了鎖

4.如果在步驟3中,發現不是最小的,那么等待,直到下次子節點變更通知的時候,在進行子節點的獲取,判斷是否獲取到鎖

5.釋放鎖也比較容易,就是刪除自己創建的那個節點即可

上面的這種思路,在集群規模很大的情況下,會出現“羊群效應”(Herd Effect):

在上面的分布式鎖的競爭中,有一個細節,就是在getChildren上注冊了子節點變更通知Watcher,這有什么問題么?這其實會導致客戶端大量重復的運行,而且絕大多數的運行結果都是判斷自己并非是序號最小的節點,從而繼續等待下一次通知,也就是很多客戶端做了很多無用功。更加要命的是,在集群規模很大的情況下,這顯然會對Server的性能造成影響,而且一旦同一個時間,多個客戶端斷開連接,服務器會向其余客戶端發送大量的事件通知,這就是所謂的羊群效應!

出現這個問題的根源,其實在于,上述的思路并沒有找準客戶端的“痛點”:

客戶端的核心訴求在于判斷自己是否是最小的節點,所以說每個節點的創建者其實不用關心所有的節點變更,它真正關心的應該是比自己序號小的那個節點是否存在!

1.client調用create()方法創建“/root/lock_”節點,注意節點類型是EPHEMERAL_SEQUENTIAL

2.client調用getChildren("/root/lock_",false)來獲取所有已經創建的子節點,這里并不注冊任何Watcher

3.客戶端獲取到所有子節點Path后,如果發現自己在步驟1中創建的節點是所有節點中最小的,那么就認為這個客戶端獲得了鎖

4.如果在步驟3中,發現不是最小的,那么找到比自己小的那個節點,然后對其調用exist()方法注冊事件監聽

5.之后一旦這個被關注的節點移除,客戶端會收到相應的通知,這個時候客戶端需要再次調用getChildren("/root/lock_",false)來確保自己是最小的節點,然后進入步驟3

OK,talk is cheap show me the code,下一篇文章會為大家帶來Zookeeper實現分布式鎖的代碼。不早啦,上班去啦!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容