在《分布式利器Zookeeper(一)》中對(duì)ZK進(jìn)行了初步的介紹以及搭建ZK集群環(huán)境,本篇博客將涉及的話題是:基于原生API方式操作ZK心铃,Watch機(jī)制钟病,分布式鎖思路探討等。
原生API操作ZK?
什么叫原生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初始化完成假勿。
對(duì)于事件(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è)對(duì)象給ZK肌索,讓ZK幫助我們完成序列化操作的蕉拢!
這里需要注意的是,原生API對(duì)于ZK的操作其實(shí)是分為同步和異步2種方式的驶社。
rc表示return code,就是返回碼测萎,0即為正常亡电。
path是傳入API的參數(shù),ctx也是傳入的參數(shù)硅瞧。
注意在刪除過程中份乒,是需要版本檢查的,所以我們一般提供-1跳過版本檢查機(jī)制腕唧。
Watch機(jī)制
ZK有watch事件或辖,是一次性觸發(fā)的。當(dāng)watch監(jiān)控的數(shù)據(jù)發(fā)生變化枣接,會(huì)通知設(shè)置了該監(jiān)控的client颂暇,即watcher。Zookeeper的watch是有自己的一些特性的:
一次性:請(qǐng)牢記但惶,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ā)生了變化财喳,而不會(huì)直接提供具體的數(shù)據(jù)內(nèi)容。
客戶端串行執(zhí)行機(jī)制:注意客戶端watch回調(diào)的過程是一個(gè)串行同步的過程斩狱,這為我們保證了順序耳高,我們也應(yīng)該意識(shí)到不能因一個(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)建污筷、修改工闺、刪除都會(huì)監(jiān)控到乍赫,而且事件類型都是一樣的,想一想如何區(qū)分呢陆蟆?給一個(gè)我的思路雷厂,就是我們得先有該path下的子節(jié)點(diǎn)的列表,然后watch觸發(fā)后叠殷,我們對(duì)比下該path下面的子節(jié)點(diǎn)SIZE大小及內(nèi)容改鲫,就知道是增加的是哪個(gè)子節(jié)點(diǎn),刪除的是哪個(gè)子節(jié)點(diǎn)了A质)
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)ゲ傩南前ⅲ胶竺鏋榇蠹医榻BCurator框架,會(huì)有更好的方式進(jìn)行處理记罚。
分布式鎖思路
首先墅诡,我們不談Zookeeper是如何幫助我們處理分布式鎖的,而是先來想一想桐智,什么是分布式鎖书斜?為什么需要分布式鎖?有哪些場(chǎng)景呢酵使?分布式鎖的使用又有哪些注意的荐吉?分布式鎖有什么特性呢?
說起鎖口渔,我們自然想到Java為我們提供的synchronized/Lock样屠,但是這顯然不夠,因?yàn)檫@只能針對(duì)一個(gè)JVM中的多個(gè)線程對(duì)共享資源的操作缺脉。那么對(duì)于多臺(tái)機(jī)器痪欲,多個(gè)進(jìn)程對(duì)同一類資源進(jìn)行操作的話,就是所謂分布式場(chǎng)景下的鎖攻礼。
各個(gè)電商平臺(tái)經(jīng)常搞的“秒殺”活動(dòng)需要對(duì)商品的庫存進(jìn)行保護(hù)业踢、12306火車票也不能多賣,更不允許一張票被多個(gè)人買到礁扮、這樣的場(chǎng)景就需要分布式鎖對(duì)共享資源進(jìn)行保護(hù)知举!
既然瞬沦,Java在分布式場(chǎng)景下的鎖已經(jīng)無能為力,那么我們只能借助其他東西了雇锡!
我們的老朋友:DB
對(duì)逛钻,沒錯(cuò),我們能否借助DB來實(shí)現(xiàn)呢锰提?要知道DB是有一些特點(diǎn)供我們利用的曙痘,比如DB本身就存在鎖機(jī)制(表鎖、行鎖)立肘,唯一約束等等边坤。
假設(shè),我們的DB中有一張表T(id谅年,methodname茧痒,ip,threadname踢故,......)文黎,其中id為主鍵惹苗,methodname為唯一索引殿较。
對(duì)于多臺(tái)機(jī)器,每臺(tái)機(jī)器上的多個(gè)線程而言桩蓉,對(duì)一個(gè)方法method進(jìn)行操作前淋纲,先select下T表中是否存在method這條記錄,如果沒有院究,就插入一條記錄到T中洽瞬。當(dāng)然可能并發(fā)select,但是由于T表的唯一約束业汰,使得只有一個(gè)請(qǐng)求能插入成功伙窃,即獲得鎖。至于釋放鎖样漆,就是方法執(zhí)行完畢后delete這條記錄即可为障。
考慮一些問題:如果DB掛了,怎么辦放祟?如果由于一些因素鳍怨,導(dǎo)致delete沒有執(zhí)行成功,那么這條記錄會(huì)導(dǎo)致該方法再也不能被訪問跪妥!為什么要先select鞋喇,為什么不直接insert呢?性能如何呢眉撵?
為了避免單點(diǎn)侦香,可以主備之間實(shí)現(xiàn)切換落塑;為了避免死鎖的產(chǎn)生,那么我們可以有一個(gè)定時(shí)任務(wù)鄙皇,定期清理T表中的記錄芜赌;先select后insert,其實(shí)是為了保證鎖的可重入性伴逸,也就是說缠沈,如果一臺(tái)IP上的某個(gè)線程獲取了鎖,那么它可以不用在釋放鎖的前提下错蝴,繼續(xù)獲得鎖洲愤;性能上,如果大量的請(qǐng)求顷锰,將會(huì)對(duì)DB考驗(yàn)柬赐,這將成為瓶頸。
到這里官紫,還有一個(gè)明顯的問題肛宋,需要我們考慮:上述的方案,雖然保證了只會(huì)有一個(gè)請(qǐng)求獲得鎖束世,但其他請(qǐng)求都獲取鎖失敗返回了酝陈,而沒有進(jìn)行鎖等待!當(dāng)然毁涉,我們可以通過重試機(jī)制沉帮,來實(shí)現(xiàn)阻塞鎖,不過數(shù)據(jù)庫本身的鎖機(jī)制可以幫助我們完成贫堰。別忘了select ... for update這種阻塞式的行鎖機(jī)制穆壕,commit進(jìn)行鎖的釋放。而且對(duì)于for update這種獨(dú)占鎖其屏,如果長(zhǎng)時(shí)間不提交釋放喇勋,會(huì)一直占用DB連接,連接爆了偎行,就跪了川背!
不說了,老朋友也只能幫我們到這里了睦优!
我們的新朋友:Redis or 其他分布式緩存(Tair/...)
既然說是緩存渗常,相較DB,有更好的性能汗盘;既然說是分布式皱碘,當(dāng)然避免了單點(diǎn)問題;
比如隐孽,用Redis作為分布式鎖的setnx癌椿,這里我就不細(xì)說了健蕊,總之分布式緩存需要特別注意的是緩存的失效時(shí)間。(有效時(shí)間過短踢俄,搞不好業(yè)務(wù)還沒有執(zhí)行完畢缩功,就釋放鎖了;有效時(shí)間過長(zhǎng)都办,其他線程白白等待嫡锌,浪費(fèi)了時(shí)間,拖慢了系統(tǒng)處理速度)
看Zookeeper是如何幫助我們實(shí)現(xiàn)分布式鎖
Zookeeper中臨時(shí)順序節(jié)點(diǎn)的特性:
第一琳钉,節(jié)點(diǎn)的生命周期和client回話綁定势木,即創(chuàng)建節(jié)點(diǎn)的客戶端回話一旦失效,那么這個(gè)節(jié)點(diǎn)就會(huì)被刪除歌懒。(臨時(shí)性)
第二啦桌,每個(gè)父節(jié)點(diǎn)都會(huì)維護(hù)子節(jié)點(diǎn)創(chuàng)建的先后順序,自動(dòng)為子節(jié)點(diǎn)分配一個(gè)整形數(shù)值及皂,以后綴的形式自動(dòng)追加到節(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)上注冊(cè)子節(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ī)模很大的情況下,會(huì)出現(xiàn)“羊群效應(yīng)”(Herd Effect):
在上面的分布式鎖的競(jìng)爭(zhēng)中摹迷,有一個(gè)細(xì)節(jié)疟赊,就是在getChildren上注冊(cè)了子節(jié)點(diǎn)變更通知Watcher,這有什么問題么峡碉?這其實(shí)會(huì)導(dǎo)致客戶端大量重復(fù)的運(yùn)行近哟,而且絕大多數(shù)的運(yùn)行結(jié)果都是判斷自己并非是序號(hào)最小的節(jié)點(diǎn),從而繼續(xù)等待下一次通知鲫寄,也就是很多客戶端做了很多無用功吉执。更加要命的是疯淫,在集群規(guī)模很大的情況下,這顯然會(huì)對(duì)Server的性能造成影響戳玫,而且一旦同一個(gè)時(shí)間熙掺,多個(gè)客戶端斷開連接,服務(wù)器會(huì)向其余客戶端發(fā)送大量的事件通知咕宿,這就是所謂的羊群效應(yīng)币绩!
出現(xiàn)這個(gè)問題的根源,其實(shí)在于府阀,上述的思路并沒有找準(zhǔn)客戶端的“痛點(diǎn)”:
客戶端的核心訴求在于判斷自己是否是最小的節(jié)點(diǎn)类浪,所以說每個(gè)節(jié)點(diǎn)的創(chuàng)建者其實(shí)不用關(guān)心所有的節(jié)點(diǎn)變更,它真正關(guān)心的應(yīng)該是比自己序號(hào)小的那個(gè)節(jié)點(diǎ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_",false)來獲取所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn),這里并不注冊(cè)任何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)不是最小的固额,那么找到比自己小的那個(gè)節(jié)點(diǎn)眠蚂,然后對(duì)其調(diào)用exist()方法注冊(cè)事件監(jiān)聽
5.之后一旦這個(gè)被關(guān)注的節(jié)點(diǎn)移除,客戶端會(huì)收到相應(yīng)的通知斗躏,這個(gè)時(shí)候客戶端需要再次調(diào)用getChildren("/root/lock_",false)來確保自己是最小的節(jié)點(diǎn)逝慧,然后進(jìn)入步驟3
OK,talk is cheap show me the code,下一篇文章會(huì)為大家?guī)鞿ookeeper實(shí)現(xiàn)分布式鎖的代碼啄糙。不早啦笛臣,上班去啦!