資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

在大數據系列第一篇的時候,搭建hdfs高可用集群的時候,講到了一個技術點,對於Java和大數據的應用都會非常的受歡迎,尤其是在面試的時候,對的,可能有的朋友已經猜到了,對的,就是分佈式協調服務--zookeeper

對於zookeeper,他是apache基金下的一個重要組成項目,而ZooKeeper是一個典型的分佈式數據一致性的解決方案。分佈式應用程序可以基於它實現諸如數據發佈/訂閱、負載均衡、命名服務、分佈式協調/通知、集群管理、Master選舉、分佈式鎖和分佈式隊列等功能。

可能這麼說還有人聽不懂,沒關係,來一個生活中的實例,每一個專業的技術總可以在生活中找到相應的實例,就比如說zookeeper,攘其外必先安其內就很好的解釋了zookeeper,相信大家都經歷過這樣一件事,選舉班長,每一次新學期開始的時候,都會選舉一個班級的管理者,班長,之後班級相應的事務就由班長進行通知和管理,對吧,我們在選舉班長的時候,都會遵循一個準則,少數服從多數,zookeeper就是扮演了這樣的一個角色,,嘿嘿嘿,有點扯遠了,跟題目有點偏離,後面我會在大數據系列中,詳細的對zookeeper內部運行流程進行整理,今天,我們的主要概念是上面提到的,zookeeper的一個功能機制,分佈式鎖

相信做過開發大家都知道,如果我們一臺機器上多個不同線程搶佔同一個資源,並且如果多次執行會有異常,我們稱之為非線程安全。一般,我們為了解決這種問題,通常使用鎖來解決,像java語言,我們可以使用synchronized。如果是同一臺機器裡面不同的java實例,我們可以使用系統的文件讀寫鎖來解決,如果再擴展到不同的機器呢?我們通常用分佈式鎖來解決。

分佈式鎖的特點如下:

  • 互斥性:和我們本地鎖一樣互斥性是最基本,但是分佈式鎖需要保證在不同節點的不同線程的互斥。
  • 可重入性:同一個節點上的同一個線程如果獲取了鎖之後那麼也可以再次獲取這個鎖。
  • 鎖超時:和本地鎖一樣支持鎖超時,防止死鎖。
  • 高效,高可用:加鎖和解鎖需要高效,同時也需要保證高可用防止分佈式鎖失效,可以增加降級。
  • 支持阻塞和非阻塞:和 ReentrantLock 一樣支持 lock 和 trylock 以及 tryLock(long timeOut)。
  • 支持公平鎖和非公平鎖(可選):公平鎖的意思是按照請求加鎖的順序獲得鎖,非公平鎖就相反是無序的。這個一般來說實現的比較少。分佈式鎖。相信大家都遇到過這樣的業務場景,我們有一個定時任務需要定時執行,但是這個任務又不是同一段時間執行冪等的,所以我們只能讓一臺機器一個線程來執行

分佈式鎖的實現有很多種,常見的有redis,zookeeper,谷歌的chubby等

今天呢?就將兩種常見的redis和zookeeper的分佈式鎖的實現原理和大家共享

簡單介紹一下。相信大家這裡已經想到了解決方案,那就是每次執行任務的時候,先查詢redis裡面是否已經有鎖的key,如果沒有就寫入,然後就開始執行任務。

這個看起來很對,不過存在什麼問題呢,例如進程A跟進程B同時查詢Redis,他們都發現Redis中沒有對應的值,然後都開始寫入,由於不是帶版本讀寫,兩個人都寫成功了,都獲得了鎖。還好,Redis給我們提供原子寫入的操作,setnx(SET if Not eXists, 一個命令我們最好把全稱也瞭解一下,有助於我們記住這個命令)。

如果你以為只要這樣就完成一個分佈式鎖,那就太天真了,我們不妨考慮一些極端情況,例如某個線程取到了鎖,但是很不幸,這個機器死機了,那麼這個鎖沒有被釋放,這個任務永遠就不會有人執行了。所以一種比較好的解決方案是,申請鎖的時候,預估一個程序的執行時間,然後給鎖設置一個超時時間,如果超過這個時間其他人也能取到這個鎖。但這又引發另外一個問題,有時候負載很高,任務執行得很慢,結果過了超時時間任務還沒執行完,這個時候又起了另外一個任務來執行。

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

架構設計的魅力正是如此,當你解決一個問題的時候,總會引發一些新的問題,需要逐步攻破逐個解決。這種方法,我們一般可以在搶佔到鎖之後,就開一個守護線程,定時去redis哪裡詢問,是不是還是由我搶佔著當前的鎖,還有多久就要過期,如果發現要過期了,就趕緊續期。

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

好了,看到這裡,相信你已經學會了如何用Redis實現一個分佈式鎖服務了

Zookeeper 實現分佈式鎖的示意圖如下:

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

上圖中左邊是Zookeeper集群, lock是數據節點,node_1到node_n表示一系列的順序臨時節點,右側client_1到client_n表示要獲取鎖的客戶端。Service是互斥訪問的服務。

下面的源碼是根據Zookeeper的開源客戶端Curator實現分佈式鎖。採用zk的原生API實現會比較複雜,所以這裡就直接用Curator這個輪子,採用Curator的acquire和release兩個方法就能實現分佈式鎖。

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

上述代碼的執行結果如下:

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

可以看到client客戶端首先拿到鎖再執行業務,然後再輪到client2嘗試獲取鎖並執行業務。

一直追蹤acquire()的加鎖方法,可以追蹤到加鎖的核心函數為attemptLock。

<code>  String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
.....
while ( !isDone )
{
isDone = true;
try
{ //創建臨時有序節點
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
//判斷自己是否最小序號的節點,如果不是添加監聽前面節點被刪的通知
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
}
//如果獲取鎖返回節點路徑
if ( hasTheLock )
{
return ourPath;
}
....
}/<code>

深入internalLockLoop函數源碼:

<code>private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception   
{
.......
while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )
{
//獲取子節點列表按照序號從小到大排序
List<string> children = getSortedChildren();
String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash

//判斷自己是否是當前最小序號節點
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
if ( predicateResults.getsTheLock() )
{
//成功獲取鎖
haveTheLock = true;
}
else
{
//拿到前一個節點
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
//如果沒有拿到鎖,調用wait,等待前一個節點刪除時,通過回調notifyAll喚醒當前線程
synchronized(this)
{
try
{
//設置監聽器,getData會判讀前一個節點是否存在,不存在就會拋出異常從而不會設置監聽器
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
//如果設置了millisToWait,等一段時間,到了時間刪除自己跳出循環
if ( millisToWait != null )
{

millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
doDelete = true; // timed out - delete our node
break;
}
//等待一段時間
wait(millisToWait);
}
else
{
//一直等待下去
wait();
}

}

catch ( KeeperException.NoNodeException e )
{
//getData發現前一個子節點被刪除,拋出異常
}
}
}
}
}
.....
}/<string>/<code>

採用zk實現分佈式鎖在實際應用中不是很常見,需要一套zk集群,而且頻繁監聽對zk集群來說也是有壓力,所以不推薦大家用。不過能去面試的時候,能具體說一下使用zk實現分佈式鎖,我想應該也是一個加分項 。


那對於zookeeper,想要實踐的小夥伴,來看這裡,安裝步驟奉上

搭建zookeeper集群 (最好配置成home,別用prefix,尤其hadoop)


上傳zookeepr包

解壓:tar -xf zookeeper-3.4.6.tar.gz

移動zookeeper包到/opt/sxt目錄下:mv zookeeper-3.4.6 /opt/sxt

配置zookeeper的環境變量:vi /etc/profile

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

配置zookeeper配置文件

進入zookeeper家目錄中conf目錄下,可看到一個zoo_sample.cfg文件

拷貝重命名:cp zoo_sample.cfg zoo.cfg

配置zoo.cfg: vi zoo.cfg

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

進入數據目錄/var/sxt/zk,執行:echo 1 > myid echo 2 > myid echo 3 > myid 分別在node02 node03 node04操作

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

資深架構師教你源碼講解zookeeper實現分佈式鎖以及集群搭建步驟

也就是創建myid文件在服務器1,2,3分別追加1,2, 3, 代表各自zookeeper的id,跟上邊zookeeper配置文件一一對應。

從node02向node03/node04分發:scp -r zookeeper-3.4.6/ node04:`pwd` (發動到當前目錄,即目標目錄與源目錄相同;也可以自定 以/開始)

還需要注意,分發的目錄後一定要加(zookeeper-3.4.6/),否則就是把該目錄的內容發過去,目錄名稱不會分發!!!


啟動zookeeper集群:

zkServer.sh start node02、node03、node04分別操作

查看zookeeper集群個節點的啟動狀態:

zkServer.sh status

zkServer.sh stop

好了,到這裡,關於分佈式鎖以及zookeeper的搭建的相關知識,更多關於zookeeper以及Java大數據的相關知識,後期會不斷進行更新,歡迎大家轉發關注哈~~


分享到:


相關文章: