溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

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

發(fā)布時間:2020-07-10 12:57:34 來源:網絡 閱讀:15950 作者:zfz_linux_boy 欄目:建站服務器

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

原生API操作ZK 

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

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

說明:

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

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

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


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


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

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

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

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


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


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

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

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

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


Watch機制

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

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

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

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

下面我們來直接看代碼:

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


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


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

一定得注意的是,監(jiān)控該節(jié)點和監(jiān)控該節(jié)點的子節(jié)點是2碼子事。

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

getChildren(path,true)和getChildren(path,watcher)有什么區(qū)別?前者是沿用上下文中的Watcher,而后者則是可以設置一個新的Watcher的!(因此,要想做到一直監(jiān)控,那么就有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中。當然可能并發(fā)select,但是由于T表的唯一約束,使得只有一個請求能插入成功,即獲得鎖。至于釋放鎖,就是方法執(zhí)行完畢后delete這條記錄即可。

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

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

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

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


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

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

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

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

Zookeeper中臨時順序節(jié)點的特性:

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

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

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

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

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

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

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

5.釋放鎖也比較容易,就是刪除自己創(chuàng)建的那個節(jié)點即可

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

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

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

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

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

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

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

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

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

OK,talk is cheap show me the code,下一篇文章會為大家?guī)鞿ookeeper實現分布式鎖的代碼。



向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI