天天看點

分布式利器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。
分布式利器Zookeeper(二):分布式鎖

說明:

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

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

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

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

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

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

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

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

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

這裡需要注意的是,原生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回調。

下面我們來直接看代碼:

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

一定得注意的是,監控該節點和監控該節點的子節點是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中有一張表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連接配接,連接配接爆了,就跪了!

不說了,老朋友也隻能幫我們到這裡了!

既然說是緩存,相較DB,有更好的性能;既然說是分布式,當然避免了單點問題; 比如,用Redis作為分布式鎖的setnx,這裡我就不細說了,總之分布式緩存需要特别注意的是緩存的失效時間。(有效時間過短,搞不好業務還沒有執行完畢,就釋放鎖了;有效時間過長,其他線程白白等待,浪費了時間,拖慢了系統處理速度)
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的性能造成影響,而且一旦同一個時間,多個用戶端斷開連接配接,伺服器會向其餘用戶端發送大量的事件通知,這就是所謂的羊群效應!

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

用戶端的核心訴求在于判斷自己是否是最小的節點,是以說每個節點的建立者其實不用關心所有的節點變更,它真正關心的應該是比自己序号小的那個節點是否存在! 2.client調用getChildren("/root/lock_",false)來擷取所有已經建立的子節點,這裡并不注冊任何Watcher 4.如果在步驟3中,發現不是最小的,那麼找到比自己小的那個節點,然後對其調用exist()方法注冊事件監聽 5.之後一旦這個被關注的節點移除,用戶端會收到相應的通知,這個時候用戶端需要再次調用getChildren("/root/lock_",false)來確定自己是最小的節點,然後進入步驟3

OK,talk is cheap show me the code,下一篇文章會為大家帶來Zookeeper實作分布式鎖的代碼。

繼續閱讀