在《分布式利器Zookeeper(一)》中對ZK進(jìn)行了初步的介紹以及搭建ZK集群環(huán)境,本篇博客將涉及的話題是:基于原生API方式操作ZK,Watch機(jī)制,分布式鎖思路探討等。
成都創(chuàng)新互聯(lián)公司專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)、平陰網(wǎng)絡(luò)推廣、小程序定制開發(fā)、平陰網(wǎng)絡(luò)營銷、平陰企業(yè)策劃、平陰品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎(jiǎng);成都創(chuàng)新互聯(lián)公司為所有大學(xué)生創(chuàng)業(yè)者提供平陰建站搭建服務(wù),24小時(shí)服務(wù)熱線:13518219792,官方網(wǎng)址:www.cdcxhl.com
什么叫原生API操作ZK呢?實(shí)際上,利用zookeeper.jar這樣的就是基于原生的API方式操作ZK,因?yàn)檫@個(gè)原生API使用起來并不是讓人很舒服,于是出現(xiàn)了zkclient這種方式,以至到后來基于Curator框架,讓人使用ZK更加方便。有一句話,Guava is to JAVA what Curator is to Zookeeper。
說明:
在初始化Zookeeper時(shí),有多種構(gòu)造方法可以選擇,有3個(gè)參數(shù)是必備的:connectionString(多個(gè)ZK SERVER之間以,分隔),sessionTimeout(就是zoo.cfg中的tickTime),Watcher(事件處理通知器)。
需要注意的是ZK的連接是異步的,因此我們需要CountDownLatch來幫助我們確保ZK初始化完成。
對于事件(WatchedEvent)而言,有狀態(tài)以及類型。
下面,我們來看一看基于原生API方式的增刪改查:
注意,節(jié)點(diǎn)有2大類型,持久化節(jié)點(diǎn)、臨時(shí)節(jié)點(diǎn)。在此基礎(chǔ)上,又可以分為持久化順序節(jié)點(diǎn)(PERSISTENT_SEQUENTIAL)、臨時(shí)順序節(jié)點(diǎn)(EPHEMERAL_SEQUENTIAL)。
節(jié)點(diǎn)類型只支持byte[],也就是說我們是無法直接給一個(gè)對象給ZK,讓ZK幫助我們完成序列化操作的!
這里需要注意的是,原生API對于ZK的操作其實(shí)是分為同步和異步2種方式的。
rc表示return code,就是返回碼,0即為正常。
path是傳入API的參數(shù),ctx也是傳入的參數(shù)。
注意在刪除過程中,是需要版本檢查的,所以我們一般提供-1跳過版本檢查機(jī)制。
ZK有watch事件,是一次性觸發(fā)的。當(dāng)watch監(jiān)控的數(shù)據(jù)發(fā)生變化,會通知設(shè)置了該監(jiān)控的client,即watcher。Zookeeper的watch是有自己的一些特性的:
一次性:請牢記,just watch one time! 因?yàn)閆K的監(jiān)控是一次性的,所以每次必須設(shè)置監(jiān)控。
輕量:WatchedEvent是ZK進(jìn)行watch通知的最小單元,整個(gè)數(shù)據(jù)結(jié)構(gòu)包含:事件狀態(tài)、事件類型、節(jié)點(diǎn)路徑。注意ZK只是通知client節(jié)點(diǎn)的數(shù)據(jù)發(fā)生了變化,而不會直接提供具體的數(shù)據(jù)內(nèi)容。
客戶端串行執(zhí)行機(jī)制:注意客戶端watch回調(diào)的過程是一個(gè)串行同步的過程,這為我們保證了順序,我們也應(yīng)該意識到不能因一個(gè)watch的回調(diào)處理邏輯而影響了整個(gè)客戶端的watch回調(diào)。
下面我們來直接看代碼:
一定得注意的是,監(jiān)控該節(jié)點(diǎn)和監(jiān)控該節(jié)點(diǎn)的子節(jié)點(diǎn)是2碼子事。
比如exists(path,true)監(jiān)控的就是該path節(jié)點(diǎn)的create/delete/setData;getChildren(path,watcher)監(jiān)控的就是該path節(jié)點(diǎn)下的子節(jié)點(diǎn)的變化(子節(jié)點(diǎn)的創(chuàng)建、修改、刪除都會監(jiān)控到,而且事件類型都是一樣的,想一想如何區(qū)分呢?給一個(gè)我的思路,就是我們得先有該path下的子節(jié)點(diǎn)的列表,然后watch觸發(fā)后,我們對比下該path下面的子節(jié)點(diǎn)SIZE大小及內(nèi)容,就知道是增加的是哪個(gè)子節(jié)點(diǎn),刪除的是哪個(gè)子節(jié)點(diǎn)了!)
getChildren(path,true)和getChildren(path,watcher)有什么區(qū)別?前者是沿用上下文中的Watcher,而后者則是可以設(shè)置一個(gè)新的Watcher的!(因此,要想做到一直監(jiān)控,那么就有2種方式,一個(gè)是注意每次設(shè)置成true,或者干脆每次設(shè)置一個(gè)新的Watcher)
從上面的討論中,你大概能了解到原生的API其實(shí)功能上還不是很強(qiáng)大,有些還得我們?nèi)ゲ傩?,到后面為大家介紹Curator框架,會有更好的方式進(jìn)行處理。
首先,我們不談Zookeeper是如何幫助我們處理分布式鎖的,而是先來想一想,什么是分布式鎖?為什么需要分布式鎖?有哪些場景呢?分布式鎖的使用又有哪些注意的?分布式鎖有什么特性呢?
說起鎖,我們自然想到Java為我們提供的synchronized/Lock,但是這顯然不夠,因?yàn)檫@只能針對一個(gè)JVM中的多個(gè)線程對共享資源的操作。那么對于多臺機(jī)器,多個(gè)進(jìn)程對同一類資源進(jìn)行操作的話,就是所謂分布式場景下的鎖。
各個(gè)電商平臺經(jīng)常搞的“秒殺”活動需要對商品的庫存進(jìn)行保護(hù)、12306火車票也不能多賣,更不允許一張票被多個(gè)人買到、這樣的場景就需要分布式鎖對共享資源進(jìn)行保護(hù)!
既然,Java在分布式場景下的鎖已經(jīng)無能為力,那么我們只能借助其他東西了!
對,沒錯(cuò),我們能否借助DB來實(shí)現(xiàn)呢?要知道DB是有一些特點(diǎn)供我們利用的,比如DB本身就存在鎖機(jī)制(表鎖、行鎖),唯一約束等等。
假設(shè),我們的DB中有一張表T(id,methodname,ip,threadname,......),其中id為主鍵,methodname為唯一索引。
對于多臺機(jī)器,每臺機(jī)器上的多個(gè)線程而言,對一個(gè)方法method進(jìn)行操作前,先select下T表中是否存在method這條記錄,如果沒有,就插入一條記錄到T中。當(dāng)然可能并發(fā)select,但是由于T表的唯一約束,使得只有一個(gè)請求能插入成功,即獲得鎖。至于釋放鎖,就是方法執(zhí)行完畢后delete這條記錄即可。
考慮一些問題:如果DB掛了,怎么辦?如果由于一些因素,導(dǎo)致delete沒有執(zhí)行成功,那么這條記錄會導(dǎo)致該方法再也不能被訪問!為什么要先select,為什么不直接insert呢?性能如何呢?
為了避免單點(diǎn),可以主備之間實(shí)現(xiàn)切換;為了避免死鎖的產(chǎn)生,那么我們可以有一個(gè)定時(shí)任務(wù),定期清理T表中的記錄;先select后insert,其實(shí)是為了保證鎖的可重入性,也就是說,如果一臺IP上的某個(gè)線程獲取了鎖,那么它可以不用在釋放鎖的前提下,繼續(xù)獲得鎖;性能上,如果大量的請求,將會對DB考驗(yàn),這將成為瓶頸。
到這里,還有一個(gè)明顯的問題,需要我們考慮:上述的方案,雖然保證了只會有一個(gè)請求獲得鎖,但其他請求都獲取鎖失敗返回了,而沒有進(jìn)行鎖等待!當(dāng)然,我們可以通過重試機(jī)制,來實(shí)現(xiàn)阻塞鎖,不過數(shù)據(jù)庫本身的鎖機(jī)制可以幫助我們完成。別忘了select ... for update這種阻塞式的行鎖機(jī)制,commit進(jìn)行鎖的釋放。而且對于for update這種獨(dú)占鎖,如果長時(shí)間不提交釋放,會一直占用DB連接,連接爆了,就跪了!
不說了,老朋友也只能幫我們到這里了!
既然說是緩存,相較DB,有更好的性能;既然說是分布式,當(dāng)然避免了單點(diǎn)問題;
比如,用Redis作為分布式鎖的setnx,這里我就不細(xì)說了,總之分布式緩存需要特別注意的是緩存的失效時(shí)間。(有效時(shí)間過短,搞不好業(yè)務(wù)還沒有執(zhí)行完畢,就釋放鎖了;有效時(shí)間過長,其他線程白白等待,浪費(fèi)了時(shí)間,拖慢了系統(tǒng)處理速度)
Zookeeper中臨時(shí)順序節(jié)點(diǎn)的特性:
第一,節(jié)點(diǎn)的生命周期和client回話綁定,即創(chuàng)建節(jié)點(diǎn)的客戶端回話一旦失效,那么這個(gè)節(jié)點(diǎn)就會被刪除。(臨時(shí)性)
第二,每個(gè)父節(jié)點(diǎn)都會維護(hù)子節(jié)點(diǎn)創(chuàng)建的先后順序,自動為子節(jié)點(diǎn)分配一個(gè)×××數(shù)值,以后綴的形式自動追加到節(jié)點(diǎn)名稱中,作為這個(gè)節(jié)點(diǎn)最終的節(jié)點(diǎn)名稱。(順序性)
那么,基于臨時(shí)順序節(jié)點(diǎn)的特性,Zookeeper實(shí)現(xiàn)分布式鎖的一般思路如下:
1.client調(diào)用create()方法創(chuàng)建“/root/lock_”節(jié)點(diǎn),注意節(jié)點(diǎn)類型是EPHEMERAL_SEQUENTIAL
2.client調(diào)用getChildren("/root/lock_",watch)來獲取所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn),并同時(shí)在這個(gè)節(jié)點(diǎn)上注冊子節(jié)點(diǎn)變更通知的Watcher
3.客戶端獲取到所有子節(jié)點(diǎn)Path后,如果發(fā)現(xiàn)自己在步驟1中創(chuàng)建的節(jié)點(diǎn)是所有節(jié)點(diǎn)中最小的,那么就認(rèn)為這個(gè)客戶端獲得了鎖
4.如果在步驟3中,發(fā)現(xiàn)不是最小的,那么等待,直到下次子節(jié)點(diǎn)變更通知的時(shí)候,在進(jìn)行子節(jié)點(diǎn)的獲取,判斷是否獲取到鎖
5.釋放鎖也比較容易,就是刪除自己創(chuàng)建的那個(gè)節(jié)點(diǎn)即可
上面的這種思路,在集群規(guī)模很大的情況下,會出現(xiàn)“羊群效應(yīng)”(Herd Effect):
在上面的分布式鎖的競爭中,有一個(gè)細(xì)節(jié),就是在getChildren上注冊了子節(jié)點(diǎn)變更通知Watcher,這有什么問題么?這其實(shí)會導(dǎo)致客戶端大量重復(fù)的運(yùn)行,而且絕大多數(shù)的運(yùn)行結(jié)果都是判斷自己并非是序號最小的節(jié)點(diǎn),從而繼續(xù)等待下一次通知,也就是很多客戶端做了很多無用功。更加要命的是,在集群規(guī)模很大的情況下,這顯然會對Server的性能造成影響,而且一旦同一個(gè)時(shí)間,多個(gè)客戶端斷開連接,
OK,talk is cheap show me the code,下一篇文章會為大家?guī)鞿ookeeper實(shí)現(xiàn)分布式鎖的代碼。