怎樣制作自己公司的網(wǎng)站西安百度關(guān)鍵詞優(yōu)化
7 分布式鎖
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問這些資源的時(shí)候,往往需要通過一些互斥手段來防止彼此之間的干擾,以保證一致性,在這種情況下,就需要使用分布式鎖了。
在平時(shí)的實(shí)際項(xiàng)目開發(fā)中,我們往往很少會去在意分布式鎖,而是依賴于關(guān)系型數(shù)據(jù)庫固有的排他性來實(shí)現(xiàn)不同進(jìn)程之間的互斥。這確實(shí)是一種非常簡便且被廣泛使用的分布式鎖實(shí)現(xiàn)方式。然而有一個(gè)不爭的事實(shí)是,目前絕大多數(shù)大型分布式系統(tǒng)的性能瓶頸都集中在數(shù)據(jù)庫操作上。因此,如果上層業(yè)務(wù)再給數(shù)據(jù)庫添加一些額外的鎖,例如行鎖、表鎖甚至是繁重的事務(wù)處理,那么是不是會讓數(shù)據(jù)庫更加不堪重負(fù)呢?下面我們來看看使用ZooKeeper如何實(shí)現(xiàn)分布式鎖,這里主要講解排他鎖和共享鎖兩類分布式鎖。
排他鎖
排他鎖(Exclusive Locks,簡稱X鎖),又稱為寫鎖或獨(dú)占鎖,是一種基本的鎖類型。
如果事務(wù)T1對數(shù)據(jù)對象O1加上了排他鎖,那么在整個(gè)加鎖期間,只允許事務(wù)T1對O1進(jìn)行讀取和更新操作,其他任何事務(wù)都不能再對這個(gè)數(shù)據(jù)對象進(jìn)行任何類型的操作——直到T1釋放了排他鎖。
從上面講解的排他鎖的基本概念中,我們可以看到,排他鎖的核心是如何保證當(dāng)前有且僅有一個(gè)事務(wù)獲得鎖,并且鎖被釋放后,所有正在等待獲取鎖的事務(wù)都能夠被通知到。
下面我們就來看看如何借助ZooKeeper實(shí)現(xiàn)排他鎖。
定義鎖
在通常的Java開發(fā)編程中,有兩種常見的方式可以用來定義鎖,分別是synchronized機(jī)制和JDK5提供的ReentrantLock。然而,在ZooKeeper中,沒有類似于這樣的API可以直接使用,而是通過ZooKeeper上的數(shù)據(jù)節(jié)點(diǎn)來表示一個(gè)鎖,例如/exclusive_lock/lock節(jié)點(diǎn)就可以被定義為一個(gè)鎖,如下圖所示。
獲取鎖
在需要獲取排他鎖時(shí),所有的客戶端都會試圖通過調(diào)用create()接口,在/exclusive_lock 節(jié)點(diǎn)下創(chuàng)建臨時(shí)子節(jié)點(diǎn)/exclusive_lock/lock。在前面幾節(jié)中我們也介紹了,ZooKeeper會保證在所有的客戶端中,最終只有一個(gè)客戶端能夠創(chuàng)建成功,那么就可以認(rèn)為該客戶端獲取了鎖。同時(shí),所有沒有獲取到鎖的客戶端就需要到/exclusive_lock節(jié)點(diǎn)上注冊一個(gè)子節(jié)點(diǎn)變更的Watcher監(jiān)聽,以便實(shí)時(shí)監(jiān)聽到lock節(jié)點(diǎn)的變更情況。
釋放鎖
在“定義鎖”部分,我們已經(jīng)提到,/exclusive_lock/lock是一個(gè)臨時(shí)節(jié)點(diǎn),因此在以下兩種情況下,都有可能釋放鎖。
(1)當(dāng)前獲取鎖的客戶端機(jī)器發(fā)生宕機(jī),那么ZooKeeper上的這個(gè)臨時(shí)節(jié)點(diǎn)就會被移除。
(2)正常執(zhí)行完業(yè)務(wù)邏輯后,客戶端就會主動將自己創(chuàng)建的臨時(shí)節(jié)點(diǎn)刪除。
無論在什么情況下移除了lock 節(jié)點(diǎn),ZooKeeper都會通知所有在/exclusive_lock節(jié)點(diǎn)上注冊了子節(jié)點(diǎn)變更Watcher監(jiān)聽的客戶端。這些客戶端在接收到通知后,再次重新發(fā)起分布式鎖獲取,即重復(fù)“獲取鎖”過程。整個(gè)排他鎖的獲取和釋放流程,可以用下圖來表示。
共享鎖
共享鎖(Shared Locks,簡稱S鎖),又稱為讀鎖,同樣是一種基本的鎖類型。如果事務(wù)T1對數(shù)據(jù)對象O1,加上了共享鎖,那么當(dāng)前事務(wù)只能對O1進(jìn)行讀取操作,其他事務(wù)也只能對這個(gè)數(shù)據(jù)對象加共享鎖一直到該數(shù)據(jù)對象上的所有共享鎖都被釋放。
共享鎖和排他鎖最根本的區(qū)別在于,加上排他鎖后,數(shù)據(jù)對象只對一個(gè)事務(wù)可見,而加上共享鎖后,數(shù)據(jù)對所有事務(wù)都可見。下面我們就來看看如何借助ZooKeeper來實(shí)現(xiàn)共享鎖。
定義鎖
和排他鎖一樣,同樣是通過ZooKeeper上的數(shù)據(jù)節(jié)點(diǎn)來表示一個(gè)鎖,是一個(gè)類似于“/shared_lock/[Hostname]-請求類型-序號”的臨時(shí)順序節(jié)點(diǎn),例如/shared_lock/192.168.0.1-0000000001,那么,這個(gè)節(jié)點(diǎn)就代表了一個(gè)共享鎖。
獲取鎖
在需要獲取共享鎖時(shí),所有客戶端都會到/shared_lock這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),如果當(dāng)前是讀請求,那么就創(chuàng)建例如/shared_lock/192. 168.0.1-0000000001的節(jié)點(diǎn);如果是寫請求,那么就創(chuàng)建例如/shared_lock/192.168.0. I-W-000000001的節(jié)點(diǎn)。
判斷讀寫順序
根據(jù)共享鎖的定義,不同的事務(wù)都可以同時(shí)對同一個(gè)數(shù)據(jù)對象進(jìn)行讀取操作,而更新操作必須在當(dāng)前沒有任何事務(wù)進(jìn)行讀寫操作的情況下進(jìn)行?;谶@個(gè)原則,我們來看看如何通過ZooKeeper的節(jié)點(diǎn)來確定分布式讀寫順序,大致可以分為如下4個(gè)步驟。
(1)創(chuàng)建完節(jié)點(diǎn)后,獲取/shared_lock節(jié)點(diǎn)下的所有子節(jié)點(diǎn),并對該節(jié)點(diǎn)注冊子節(jié)點(diǎn)變更的Watcher監(jiān)聽。
(2)確定自己的節(jié)點(diǎn)序號在所有子節(jié)點(diǎn)中的順序。
(3)對于讀請求:
如果沒有比自己序號小的子節(jié)點(diǎn),或是所有比自己序號小的子節(jié)點(diǎn)都是讀請求,那么表明自己已經(jīng)成功獲取到了共享鎖,同時(shí)開始執(zhí)行讀取邏輯。
如果比自己序號小的子節(jié)點(diǎn)中有寫請求,那么就需要進(jìn)入等待。
對于寫請求:
如果自己不是序號最小的子節(jié)點(diǎn),那么就需要進(jìn)入等待。
(4)接收到Watcher通知后,重復(fù)步驟1。
釋放鎖
釋放鎖的邏輯和排他鎖是一致的,這里不再贅述。整個(gè)共享鎖的獲取和釋放流程,可以用下圖來表示。
羊群效應(yīng)
上面講解的這個(gè)共享鎖實(shí)現(xiàn),大體上能夠滿足一般的分布式集群競爭鎖的需求,并且性能都還可以一這里說的一般場景是指集群規(guī)模不是特別大,一般是在10臺機(jī)器以內(nèi)。
但是如果機(jī)器規(guī)模擴(kuò)大之后,會有什么問題呢?我們著重來看上面“判斷讀寫順序”程的步驟3,結(jié)合下圖給出的實(shí)例,看看實(shí)際運(yùn)行中的情況。
針對上圖中的實(shí)際情況,我們看看會發(fā)生什么事情。
(1)192.168.0.1這臺機(jī)器首先進(jìn)行讀操作,完成讀操作后將節(jié)點(diǎn)/192.168.0.1-R-0000000001刪除。
(2)余下的4臺機(jī)器均收到了這個(gè)節(jié)點(diǎn)被移除的通知,然后重新從/shared_lock節(jié)點(diǎn)上獲取一份新的子節(jié)點(diǎn)列表。
(3)每個(gè)機(jī)器判斷自己的讀寫順序。其中192.168.0.2這臺機(jī)器檢測到自己已經(jīng)是序號最小的機(jī)器了,于是開始進(jìn)行寫操作,而余下的其他機(jī)器發(fā)現(xiàn)沒有輪到自己進(jìn)行讀取或更新操作,于是繼續(xù)等待。
(4)繼續(xù)....
上面這個(gè)過程就是共享鎖在實(shí)際運(yùn)行中最主要的步驟了,我們著重看下上面步驟3中提
到的:“而余下的其他機(jī)器發(fā)現(xiàn)沒有輪到自己進(jìn)行讀取或更新操作,于是繼續(xù)等待?!焙?/p>
明顯,我們看到,192.168.0.1 這個(gè)客戶端在移除自己的共享鎖后,ZooKeeper 發(fā)送了子
節(jié)點(diǎn)變更Watcher通知給所有機(jī)器,然而這個(gè)通知除了給192.168.0.2這臺機(jī)器產(chǎn)生實(shí)際
影響外,對于余下的其他所有機(jī)器都沒有任何作用。
相信讀者也已經(jīng)意識到了,在這整個(gè)分布式鎖的競爭過程中,大量的“Watcher通知”和“子節(jié)點(diǎn)列表獲取”兩個(gè)操作重復(fù)運(yùn)行,并且絕大多數(shù)的運(yùn)行結(jié)果都是判斷出自己并非是序號最小的節(jié)點(diǎn),從而繼續(xù)等待下一次通知——這個(gè)看起來顯然不怎么科學(xué)。客戶端無端地接收到過多和自己并不相關(guān)的事件通知,如果在集群規(guī)模比較大的情況下,不僅會對ZooKeeper服務(wù)器造成巨大的性能影響和網(wǎng)絡(luò)沖擊,更為嚴(yán)重的是,如果同一時(shí)間有多個(gè)節(jié)點(diǎn)對應(yīng)的客戶端完成事務(wù)或是事務(wù)中斷引起節(jié)點(diǎn)消失,ZooKeeper服務(wù)器就會在短時(shí)間內(nèi)向其余客戶端發(fā)送大量的事件通知一這就是所謂的羊群效應(yīng)。
上面這個(gè)ZooKeeper分布式共享鎖實(shí)現(xiàn)中出現(xiàn)羊群效應(yīng)的根源在于,沒有找準(zhǔn)客戶端真正的關(guān)注點(diǎn)。我們再來回顧一下上面的分布式鎖競爭過程,它的核心邏輯在于:判斷自己是否是所有子節(jié)點(diǎn)中序號最小的。于是,很容易可以聯(lián)想到,每個(gè)節(jié)點(diǎn)對應(yīng)的客戶端只需要關(guān)注比自己序號小的那個(gè)相關(guān)節(jié)點(diǎn)的變更情況就可以了一而不需要關(guān)注全局的子列表變更情況。
改進(jìn)后的分布式鎖實(shí)現(xiàn)
現(xiàn)在我們來看看如何改進(jìn)上面的分布式鎖實(shí)現(xiàn)。首先,我們需要肯定的一點(diǎn)是,上面提到的共享鎖實(shí)現(xiàn),從整體思路上來說完全正確。這里主要的改動在于:每個(gè)鎖競爭者,只需要關(guān)注/shared.lock節(jié)點(diǎn)下序號比自己小的那個(gè)節(jié)點(diǎn)是否存在即可,具體實(shí)現(xiàn)如下。
(1)客戶端調(diào)用create()方法創(chuàng)建一個(gè)類似于“/shared_ lock/[Hostname]請求類型-序號”的臨時(shí)順序節(jié)點(diǎn)。
(2)客戶端調(diào)用getChildren()接口來獲取所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn)列表,注意,這里不注冊任何Watcher。
(3)如果無法獲取共享鎖,那么就調(diào)用exist()來對比自己小的那個(gè)節(jié)點(diǎn)注冊Watcher。
注意,這里“比自己小的節(jié)點(diǎn)”只是一個(gè)籠統(tǒng)的說法,具體對于讀請求和寫請求不一樣。
讀請求:向比自己序號小的最后一個(gè)寫請求節(jié)點(diǎn)注冊Watcher監(jiān)聽。
寫請求:向比自己序號小的最后一個(gè)節(jié)點(diǎn)注冊Watcher 監(jiān)聽。
(4)等待Watcher通知,繼續(xù)進(jìn)入步驟2。
改進(jìn)后的分布式鎖流程如下圖所示。
8 分布式隊(duì)列
業(yè)界有不少分布式隊(duì)列產(chǎn)品,不過絕大多數(shù)都是類似于ActiveMQ、Metamorphosis、Kafka和HornetQ等的消息中間件(或稱為消息隊(duì)列)。在本文中,我們主要介紹基于ZooKeeper實(shí)現(xiàn)的分布式隊(duì)列。分布式隊(duì)列,簡單地講分為兩大類,一種是常規(guī)的先入先出隊(duì)列,另一種則是要等到隊(duì)列元素集聚之后才統(tǒng)一安排執(zhí)行的Barrier模型。
FIFO:先入先出
FIFO (First Input First Output,先入先出)的算法思想,以其簡單明了的特點(diǎn),廣泛應(yīng)用于計(jì)算機(jī)科學(xué)的各個(gè)方面。而FIFO隊(duì)列也是一種非常典型且應(yīng)用廣泛的按序執(zhí)行的隊(duì)列模型:先進(jìn)入隊(duì)列的請求操作先完成后,才會開始處理后面的請求。
使用ZooKeeper實(shí)現(xiàn)FIFO隊(duì)列,和前面共享鎖的實(shí)現(xiàn)非常類似。FIFO 隊(duì)列就類似于一個(gè)全寫的共享鎖模型,大體的設(shè)計(jì)思路其實(shí)非常簡單:所有客戶端都會到/queue_fifo這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),例如/queue. fifo/192. 168.0.1-0000000001。
創(chuàng)建完節(jié)點(diǎn)之后,根據(jù)如下4個(gè)步驟來確定執(zhí)行順序。
(1)通過調(diào)用getChildren()接口來獲取/queue_fifo節(jié)點(diǎn)下的所有子節(jié)點(diǎn),即獲取隊(duì)列中所有的元素。
(2)確定自己的節(jié)點(diǎn)序號在所有子節(jié)點(diǎn)中的順序。
(3)如果自己不是序號最小的子節(jié)點(diǎn),那么就需要進(jìn)入等待,同時(shí)向比自己序號小的最后一個(gè)節(jié)點(diǎn)注冊Watcher監(jiān)聽。
(4)接收到Watcher通知后,重復(fù)步驟1。
整個(gè)FIFO隊(duì)列的工作流程,可以用下圖來表示。
Barrier:分布式屏障
Barrier原意是指障礙物、屏障,而在分布式系統(tǒng)中,特指系統(tǒng)之間的一個(gè)協(xié)調(diào)條件,規(guī)定了一個(gè)隊(duì)列的元素必須都集聚后才能統(tǒng)一進(jìn)行安排, 否則一直等待。這往往出現(xiàn)在那些大規(guī)模分布式并行計(jì)算的應(yīng)用場景上:最終的合并計(jì)算需要基于很多并行計(jì)算的子結(jié)果來進(jìn)行。這些隊(duì)列其實(shí)是在FIFO 隊(duì)列的基礎(chǔ)上進(jìn)行了增強(qiáng),大致的設(shè)計(jì)思想如下:開始時(shí),/queue_barrier節(jié)點(diǎn)是一個(gè)已經(jīng)存在的默認(rèn)節(jié)點(diǎn),并且將其節(jié)點(diǎn)的數(shù)據(jù)內(nèi)容賦值為一個(gè)數(shù)字n來代表Barrier 值,例如n=10表示只有當(dāng)/queue_barrier節(jié)點(diǎn)下的子節(jié)點(diǎn)個(gè)數(shù)達(dá)到10后,才會打開Barrier。之后,所有的客戶端都會到/queue_barrier節(jié)點(diǎn)下創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),例如/queue_barrier/192.168.0.1。
創(chuàng)建完節(jié)點(diǎn)之后,根據(jù)如下5個(gè)步驟來確定執(zhí)行順序。
(1)通過調(diào)用getData()接口獲取/queue_barrier節(jié)點(diǎn)的數(shù)據(jù)內(nèi)容:10。
(2)通過調(diào)用getChildren()接口獲取/queue_barrier節(jié)點(diǎn)下的所有子節(jié)點(diǎn),即獲取隊(duì)列中的所有元素,同時(shí)注冊對子節(jié)點(diǎn)列表變更的Watcher監(jiān)聽。
(3)統(tǒng)計(jì)子節(jié)點(diǎn)的個(gè)數(shù)。
(4)如果子節(jié)點(diǎn)個(gè)數(shù)還不足10個(gè),那么就需要進(jìn)入等待。
(5)接收到Watcher通知后,重復(fù)步驟2。