程序員快速提升:精通Zookeeper的經(jīng)典應(yīng)用場(chǎng)景
內(nèi)容一:(補(bǔ)充)zookeeper集群的工作原理
zookeeper提供了重要的分布式協(xié)調(diào)服務(wù),它是如何保證集群數(shù)據(jù)的一致性的?
① ZAB協(xié)議的簡(jiǎn)單描述

ZAB(zookeeper atomic broadcast)---zookeeper 原子消息廣播協(xié)議是專門為zookeeper設(shè)計(jì)的數(shù)據(jù)一致性協(xié)議,注意此協(xié)議最主要的關(guān)注點(diǎn)在于數(shù)據(jù)一致性,而無(wú)關(guān)乎于數(shù)據(jù)的準(zhǔn)確性,權(quán)威性,實(shí)時(shí)性。
ZAB協(xié)議過(guò)程

1.所有事務(wù)轉(zhuǎn)發(fā)給leader(當(dāng)我們的follower接收到事務(wù)請(qǐng)求)
2.Leader分配全局單調(diào)遞增事務(wù)id(zxid,也就是類似于paxos算法的編號(hào)n),廣播協(xié)議提議
3.Follower處理提議,作出反饋(也就是承諾只接受比現(xiàn)在的n編號(hào)大的
4.leader收到過(guò)半數(shù)的反饋,廣播commit,把數(shù)據(jù)徹底持久化(和2pc不同的是,2pc是要等待所有小弟反饋同意)5.leader對(duì)原來(lái)轉(zhuǎn)發(fā)事務(wù)的followe進(jìn)行響應(yīng),follower也順帶把響應(yīng)返回給客戶端復(fù)制代碼
還記得我們說(shuō)過(guò)zookeeper比較適合讀比較多,寫比較少的場(chǎng)景嗎,為什么我們說(shuō)它效率高,我們可以知道,所有的事務(wù)請(qǐng)求,必須由一個(gè)全局唯一的服務(wù)器進(jìn)行協(xié)調(diào),這個(gè)服務(wù)器也就是現(xiàn)在的leader,leader服務(wù)器把客戶端的一個(gè)寫請(qǐng)求事務(wù)變成一個(gè)提議,這個(gè)提議通過(guò)我們的原子廣播協(xié)議廣播到我們服務(wù)器的其他節(jié)點(diǎn)上去,此時(shí)這個(gè)協(xié)議的編號(hào),也就是zxid肯定是最大的。
由于我們的zxid都是由leader管理的,在上一節(jié)也是講過(guò),leader之所以能成為leader,本來(lái)就是因?yàn)樗膠xid最大,此時(shí)的事務(wù)請(qǐng)求過(guò)來(lái),leader的zxid本身最大的基礎(chǔ)上再遞增,這樣新過(guò)來(lái)的事務(wù)的zxid肯定就是最大的。那么一連串的事務(wù)又是如何在leader中進(jìn)行處理,leader中會(huì)內(nèi)置一個(gè)隊(duì)列,隊(duì)列的作用就是用來(lái)保證有序性(zxid有序且隊(duì)列先進(jìn)先出原則),所以后面來(lái)的事務(wù)不可能跳過(guò)前面來(lái)的事務(wù)。所以這也是ZAB協(xié)議的一個(gè)重要特性---有序性
② Leader崩潰時(shí)的舉措
leader服務(wù)器崩潰,或者說(shuō)由于網(wǎng)絡(luò)原因?qū)е耹eader失去了與過(guò)半follower的聯(lián)系,那么就會(huì)進(jìn)入崩潰恢復(fù)模式
我們回到上一節(jié)配置集群節(jié)點(diǎn)配置時(shí),提到了在配置各節(jié)點(diǎn)時(shí)

此時(shí)第二個(gè)port,就是崩潰恢復(fù)模式要使用到的

所以此時(shí)我們ZAB協(xié)議的選舉算法應(yīng)該滿足:確保提交已經(jīng)被leader提交的事務(wù)proposal,同時(shí)丟棄已經(jīng)被跳過(guò)的事務(wù)proposal
如果讓leader選舉算法能夠保證新選舉出來(lái)的leader擁有集群中所有機(jī)器的最高zxid的事務(wù)proposal,那么就可以保證這個(gè)新選舉出來(lái)的leader一定具有所有已經(jīng)提交的提案,同時(shí)如果讓擁有最高編號(hào)的事務(wù)proposal的機(jī)器來(lái)成為leader,就可以省去leader檢查事務(wù)proposal的提交和丟棄事務(wù)proposal的操作。
③ ZAB協(xié)議的數(shù)據(jù)同步
leader選舉完成后,需要進(jìn)行follower和leader的數(shù)據(jù)同步,當(dāng)半數(shù)的follower完成同步,則可以開始提供服務(wù)。
數(shù)據(jù)同步過(guò)程

④ ZAB協(xié)議中丟棄事務(wù)proposal
zxid=高32位+低32位=leader周期編號(hào)+事務(wù)proposal編號(hào)復(fù)制代碼
事務(wù)編號(hào)zxid是一個(gè)64位的數(shù)字,低32位是一個(gè)簡(jiǎn)單的單調(diào)遞增的計(jì)數(shù)器,針對(duì)客戶端的每一個(gè)事務(wù)請(qǐng)求,leader產(chǎn)生新的事務(wù)proposal的時(shí)候都會(huì)對(duì)該計(jì)數(shù)器進(jìn)行+1的操作,高32位代表了leader周期紀(jì)元的編號(hào)。
每當(dāng)選舉產(chǎn)生一個(gè)新的leader,都會(huì)從這個(gè)leader服務(wù)器上取出其本地日志中最大事務(wù)proposal的zxid,并從zxid解析出對(duì)應(yīng)的紀(jì)元值,然后對(duì)其進(jìn)行+1操作,之后以此編號(hào)作為新的紀(jì)元,并將低32位重置為0開始生產(chǎn)新的zxid。
基于此策略,當(dāng)一個(gè)包含了上一個(gè)leader周期中尚未提交過(guò)的事務(wù)proposal的服務(wù)器啟動(dòng)加入到集群中,發(fā)現(xiàn)此時(shí)集群中已經(jīng)存在leader,將自身以follower角色連接上leader服務(wù)器后,leader服務(wù)器會(huì)根據(jù)自身最后被提交的proposal和這個(gè)follower的proposal進(jìn)行比對(duì),發(fā)現(xiàn)這個(gè)follower中有上一個(gè)leader周期的事務(wù)proposal后,leader會(huì)要求follower進(jìn)行一個(gè)回退操作,回到一個(gè)確實(shí)被集群過(guò)半機(jī)器提交的最新的事務(wù)proposal
⑤ zookeeper的可配置參數(shù)
可以從官網(wǎng)上了解zookeeper的可配置參數(shù)
zookeeper.apache.org/doc/current…
雖然是全英,但是當(dāng)大家有需要使用到它們的時(shí)候,那英文就自然不成問(wèn)題了是吧
內(nèi)容二:zookeeper的典型應(yīng)用場(chǎng)景
數(shù)據(jù)發(fā)布訂閱命名服務(wù)master選舉集群管理分布式隊(duì)列分布式鎖復(fù)制代碼
1.分布式隊(duì)列的應(yīng)用場(chǎng)景
① 業(yè)務(wù)解耦
實(shí)現(xiàn)應(yīng)用之間的解耦,這時(shí)所有的下游系統(tǒng)都訂閱隊(duì)列,從而獲得一份實(shí)時(shí)完整的數(shù)據(jù)

解耦的應(yīng)用非常廣泛,比如我們常見的發(fā)貨系統(tǒng)和訂單系統(tǒng),以前業(yè)務(wù)串行的時(shí)候,發(fā)貨系統(tǒng)一定要等訂單系統(tǒng)生成完對(duì)應(yīng)的訂單才會(huì)進(jìn)行發(fā)貨。這樣如果訂單系統(tǒng)崩潰,那發(fā)貨系統(tǒng)也無(wú)法正常運(yùn)作,引入消息隊(duì)列后,發(fā)貨系統(tǒng)是正常處理掉發(fā)貨的請(qǐng)求,再把已發(fā)貨的消息存入消息隊(duì)列,等待訂單系統(tǒng)去更新并生成訂單,但是此時(shí),訂單系統(tǒng)就算崩潰掉,我們也不會(huì)一直不發(fā)貨。
② 異步處理

可以看到在此場(chǎng)景中隊(duì)列被用于實(shí)現(xiàn)服務(wù)的異步處理,這樣做的好處在于我們可以更快地返回結(jié)果和減少等待,實(shí)現(xiàn)步驟之間的并發(fā),提升了系統(tǒng)的總體性能等
② 流量削峰

2.zk的分布式隊(duì)列
① 邏輯分析
順序節(jié)點(diǎn)的應(yīng)用,類似于我們?cè)谟脄ookeeper實(shí)現(xiàn)分布式鎖的時(shí)候如何去處理驚群效應(yīng)的做法。 且根據(jù)隊(duì)列的特點(diǎn):FIFO(先進(jìn)先出),入隊(duì)時(shí)我們創(chuàng)建順序節(jié)點(diǎn)(ps:為什么上面我們是用了順序節(jié)點(diǎn)而不是說(shuō)是臨時(shí)順序節(jié)點(diǎn),是因?yàn)槲覀兏静豢紤]客戶端掛掉的情況)并把元素傳入隊(duì)列,出隊(duì)時(shí)我們?nèi)〕鲎钚〉墓?jié)點(diǎn)。使用watch機(jī)制來(lái)監(jiān)聽隊(duì)列的狀態(tài),在隊(duì)列滿時(shí)進(jìn)行阻塞,在隊(duì)列空時(shí)進(jìn)行寫入即可。
入隊(duì)操作

如上圖,我們生產(chǎn)者需要對(duì)資源進(jìn)行訪問(wèn)時(shí),會(huì)申請(qǐng)獲取一個(gè)分布式鎖,如果未成功搶占鎖,就會(huì)進(jìn)行阻塞,搶到鎖的生產(chǎn)者會(huì)嘗試把任務(wù)提交到消息隊(duì)列,此時(shí)又會(huì)進(jìn)行判斷,如果隊(duì)列滿了,就監(jiān)聽隊(duì)列中的消費(fèi)事件,當(dāng)有消費(fèi)隊(duì)列存在空位時(shí)進(jìn)行入隊(duì),未消費(fèi)時(shí)阻塞。入隊(duì)時(shí)它會(huì)進(jìn)行釋放鎖的操作,喚醒之前搶占鎖的請(qǐng)求,并讓之后的生產(chǎn)者來(lái)獲取。
出隊(duì)操作
出隊(duì)和入隊(duì)的機(jī)制是十分相似的。

② JDK阻塞隊(duì)列操作
阻塞隊(duì)列:BlockingQueue---線程安全的阻塞隊(duì)列
它以4種形式出現(xiàn),對(duì)于不能立即滿足但是在將來(lái)某一時(shí)刻可能滿足的操作,4種形式的處理方式皆不同
1.拋出一個(gè)異常2.返回一個(gè)特殊值,true or false3.在操作可以成功前,無(wú)限阻塞當(dāng)前線程4.放棄前只在給定的最大時(shí)間限制內(nèi)阻塞復(fù)制代碼

我們將會(huì)實(shí)現(xiàn)這個(gè)阻塞隊(duì)列接口來(lái)實(shí)現(xiàn)我們的分布式隊(duì)列
內(nèi)容三:分布式隊(duì)列的代碼實(shí)現(xiàn)
public class ZkDistributeQueue extends AbstractQueue
繼承了AbstractQueue,可以省略部分基礎(chǔ)實(shí)現(xiàn)
① 基本的配置信息及使用到的參數(shù)

首先我們需要一個(gè)zkClient的客戶端,然后queueRootNode是分布式隊(duì)列的存放元素的位置,指定了一個(gè)默認(rèn)的根目錄default_queueRootNode,把隊(duì)列中的元素存放于/distributeQueue下,寫鎖節(jié)點(diǎn)代表往隊(duì)列中存放元素,讀鎖節(jié)點(diǎn)代表從隊(duì)列中去取元素,這個(gè)設(shè)計(jì)簡(jiǎn)單點(diǎn)來(lái)說(shuō)就是,queueRootNode作為最大的目錄,其下有3個(gè)子目錄,分別是queueWriteLockNode,queueReadLockNode和queueElementNode,其他的就是一些需要使用到的配置信息
② 構(gòu)造器
提供兩個(gè)構(gòu)造方法,一個(gè)為使用默認(rèn)參數(shù)實(shí)現(xiàn),另外一個(gè)是自定義實(shí)現(xiàn)

此時(shí)在我們分布式鎖的構(gòu)造器中,createPersistent()的參數(shù)true是指如果我父目錄queueRootNode并沒(méi)有事先創(chuàng)建完成,這個(gè)方法會(huì)自動(dòng)創(chuàng)建出父目錄,這樣就不怕我們?cè)谂艹绦蛑斑z漏掉一些創(chuàng)建文件結(jié)構(gòu)的工作

③ 初始化隊(duì)列信息的init()方法
重新定義好讀鎖寫寫鎖和任務(wù)存放路徑,然后把zkClient連接上,創(chuàng)建queueElementNode作為任務(wù)元素目錄,參數(shù)true上文作用已經(jīng)提到了

④ 使用put()方法進(jìn)行隊(duì)列元素入隊(duì)操作

checkElement()方法是一個(gè)簡(jiǎn)單的參數(shù)檢查,我們也可以定義有關(guān)于znode的命名規(guī)范的一些檢查,不過(guò)一般情況下只要是String類型的參數(shù)都是沒(méi)有問(wèn)題的

size()方法也很簡(jiǎn)單,就是先取得父目錄然后調(diào)用zkClient自帶的countChildren()方法得出結(jié)果返回即可

主要就是通過(guò)subscribeChildChanges()監(jiān)聽子節(jié)點(diǎn)的數(shù)據(jù)變化,在size() < capacity條件成立時(shí),就會(huì)喚醒等待隊(duì)列,而當(dāng)size() >= capacity,就會(huì)判斷隊(duì)列已經(jīng)被填滿,從而進(jìn)行阻塞

在waitForRemove()方法執(zhí)行后,我們的等待線程被喚醒,這時(shí)重新執(zhí)行put(e),嘗試重新入隊(duì)
入隊(duì)操作由enqueue(e)來(lái)完成,就是創(chuàng)建順序節(jié)點(diǎn)的步驟

⑤ 消費(fèi)操作take

附:生產(chǎn)者和消費(fèi)者的模擬
① 生產(chǎn)者
模擬了兩臺(tái)服務(wù)器,兩個(gè)并發(fā),每睡3秒鐘就往消息隊(duì)列put


② 消費(fèi)者


執(zhí)行結(jié)果
① 先執(zhí)行生產(chǎn)者

此時(shí)沒(méi)有消費(fèi)者去進(jìn)行消費(fèi),所以隊(duì)列沒(méi)一下子就滿了,我們需要注意,阻塞的不僅僅是隊(duì)列,分布式鎖也被阻塞了。

② 啟動(dòng)消費(fèi)者

基本上是生產(chǎn)者放入一個(gè)消費(fèi)者就消費(fèi)一個(gè)的狀態(tài)。從而證明該分布式隊(duì)列已經(jīng)正常工作了