程序員快速提升:精通Zookeeper的經典應用場景
內容一:(補充)zookeeper集群的工作原理
zookeeper提供了重要的分布式協調服務,它是如何保證集群數據的一致性的?
① ZAB協議的簡單描述

ZAB(zookeeper atomic broadcast)---zookeeper 原子消息廣播協議是專門為zookeeper設計的數據一致性協議,注意此協議最主要的關注點在于數據一致性,而無關乎于數據的準確性,權威性,實時性。
ZAB協議過程

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

此時第二個port,就是崩潰恢復模式要使用到的

所以此時我們ZAB協議的選舉算法應該滿足:確保提交已經被leader提交的事務proposal,同時丟棄已經被跳過的事務proposal
如果讓leader選舉算法能夠保證新選舉出來的leader擁有集群中所有機器的最高zxid的事務proposal,那么就可以保證這個新選舉出來的leader一定具有所有已經提交的提案,同時如果讓擁有最高編號的事務proposal的機器來成為leader,就可以省去leader檢查事務proposal的提交和丟棄事務proposal的操作。
③ ZAB協議的數據同步
leader選舉完成后,需要進行follower和leader的數據同步,當半數的follower完成同步,則可以開始提供服務。
數據同步過程

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

解耦的應用非常廣泛,比如我們常見的發貨系統和訂單系統,以前業務串行的時候,發貨系統一定要等訂單系統生成完對應的訂單才會進行發貨。這樣如果訂單系統崩潰,那發貨系統也無法正常運作,引入消息隊列后,發貨系統是正常處理掉發貨的請求,再把已發貨的消息存入消息隊列,等待訂單系統去更新并生成訂單,但是此時,訂單系統就算崩潰掉,我們也不會一直不發貨。
② 異步處理

可以看到在此場景中隊列被用于實現服務的異步處理,這樣做的好處在于我們可以更快地返回結果和減少等待,實現步驟之間的并發,提升了系統的總體性能等
② 流量削峰

2.zk的分布式隊列
① 邏輯分析
順序節點的應用,類似于我們在用zookeeper實現分布式鎖的時候如何去處理驚群效應的做法。 且根據隊列的特點:FIFO(先進先出),入隊時我們創建順序節點(ps:為什么上面我們是用了順序節點而不是說是臨時順序節點,是因為我們根本不考慮客戶端掛掉的情況)并把元素傳入隊列,出隊時我們取出最小的節點。使用watch機制來監聽隊列的狀態,在隊列滿時進行阻塞,在隊列空時進行寫入即可。
入隊操作

如上圖,我們生產者需要對資源進行訪問時,會申請獲取一個分布式鎖,如果未成功搶占鎖,就會進行阻塞,搶到鎖的生產者會嘗試把任務提交到消息隊列,此時又會進行判斷,如果隊列滿了,就監聽隊列中的消費事件,當有消費隊列存在空位時進行入隊,未消費時阻塞。入隊時它會進行釋放鎖的操作,喚醒之前搶占鎖的請求,并讓之后的生產者來獲取。
出隊操作
出隊和入隊的機制是十分相似的。

② JDK阻塞隊列操作
阻塞隊列:BlockingQueue---線程安全的阻塞隊列
它以4種形式出現,對于不能立即滿足但是在將來某一時刻可能滿足的操作,4種形式的處理方式皆不同
1.拋出一個異常2.返回一個特殊值,true or false3.在操作可以成功前,無限阻塞當前線程4.放棄前只在給定的最大時間限制內阻塞復制代碼

我們將會實現這個阻塞隊列接口來實現我們的分布式隊列
內容三:分布式隊列的代碼實現
public class ZkDistributeQueue extends AbstractQueue
繼承了AbstractQueue,可以省略部分基礎實現
① 基本的配置信息及使用到的參數

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

此時在我們分布式鎖的構造器中,createPersistent()的參數true是指如果我父目錄queueRootNode并沒有事先創建完成,這個方法會自動創建出父目錄,這樣就不怕我們在跑程序之前遺漏掉一些創建文件結構的工作

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

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

checkElement()方法是一個簡單的參數檢查,我們也可以定義有關于znode的命名規范的一些檢查,不過一般情況下只要是String類型的參數都是沒有問題的

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

主要就是通過subscribeChildChanges()監聽子節點的數據變化,在size() < capacity條件成立時,就會喚醒等待隊列,而當size() >= capacity,就會判斷隊列已經被填滿,從而進行阻塞

在waitForRemove()方法執行后,我們的等待線程被喚醒,這時重新執行put(e),嘗試重新入隊
入隊操作由enqueue(e)來完成,就是創建順序節點的步驟

⑤ 消費操作take

附:生產者和消費者的模擬
① 生產者
模擬了兩臺服務器,兩個并發,每睡3秒鐘就往消息隊列put


② 消費者


執行結果
① 先執行生產者

此時沒有消費者去進行消費,所以隊列沒一下子就滿了,我們需要注意,阻塞的不僅僅是隊列,分布式鎖也被阻塞了。

② 啟動消費者

基本上是生產者放入一個消費者就消費一個的狀態。從而證明該分布式隊列已經正常工作了