curator實現分佈式鎖

轉https://blog.csdn.net/liyiming2017/article/details/83896169

[轉自](https://blog.csdn.net/liyiming2017/article/details/83896169)

目錄:

上一篇文章中,我們使用zookeeper的java api實現了分佈式排他鎖。其實zookeeper有一個優秀的框架---Curator,提供了各種分佈式協調的服務。Curator中有着更爲標準、規範的分佈式鎖實現。與其我們自己去實現,不如直接使用Curator。通過學習Curator的源代碼,我們也能瞭解實現分佈式鎖的最佳實踐。

-----------------------------------------------------------------------------------

ZooKeeper框架Curator分佈式鎖實現及源代碼分析

Curator中有各種分佈式鎖,本文挑選其中一個---InterProcessMutex進行講解。

我們先看一下Curator代碼中對於InterProcessMutex的註釋:

可重入的互斥鎖,跨JVM工作。使用ZooKeeper來控制鎖。所有JVM中的任何進程,只要使用同樣的鎖路徑,將會成爲跨進程的一部分。此外,這個排他鎖是“公平的”,每個用戶按照申請的順序得到排他鎖。

可見InterProcessMutex和我們自己實現的例子都是一個排他鎖,此外還可以重入。 

 

如何使用InterProcessMutex

在分析InterProcessMutex代碼前,我們先看一下它是如何使用的,下面代碼簡單展示了InterProcessMutex的使用:

  1. public static void soldTickWithLock(CuratorFramework client) throws Exception {
  2. //創建分佈式鎖, 鎖空間的根節點路徑爲/curator/lock
  3. InterProcessMutex mutex = new InterProcessMutex(client, "/curator/locks");
  4. mutex.acquire();
  5. //獲得了鎖, 進行業務流程
  6. //代表複雜邏輯執行了一段時間
  7. int sleepMillis = (int) (Math.random() * 2000);
  8. Thread.sleep(sleepMillis);
  9. //完成業務流程, 釋放鎖
  10. mutex.release();
  11. }

使用方式和我們自己編寫的鎖是一樣的,首先通過mutex.acquire()獲取鎖,該方法會阻塞進程,直到獲取鎖,然後執行你的業務方法,最後通過 mutex.release()釋放鎖。

接下來我們進入正題,展開分析Curator關於分佈式鎖的實現:

 

實現思路

Curator設計方式和之前我們自己實現的方式是類似的:

1、創建有序臨時節點

2、觸發“嘗試取鎖邏輯”,如果自己是臨時鎖節點序列的第一個,則取得鎖,獲取鎖成功。

3、如果自己不是序列中第一個,則監聽前一個鎖節點變更。同時阻塞線程。

4、當前一個鎖節點變更時,通過watcher恢復線程,然後再次到步驟2“嘗試取鎖邏輯”

如下圖所示:

 

 代碼實現概述

Curator對於排它鎖的頂層實現邏輯在InterProcessMutex類中,它對客戶端暴露鎖的使用方法,如獲取鎖和釋放鎖等。但鎖的上述實現邏輯,是由他持有的LockInternals對象來具體實現的。LockInternals使用StandardLockInternalsDriver類中的方法來做一些處理。

簡單點解釋,我們打個比方,Curator好比是一家公司承接各種業務,InterProcessMutex是老闆,收到自己客戶(client)的需求後,分配給自己的下屬LockInternals去具體完成,同時給他一個工具StandardLockInternalsDriver,讓他在做任務的過程中使用。如下圖展示:

接下來我們將深入分析InterProcessMutex、LockInternals及StandardLockInternalsDriver類。

 

InterProcessMutex源碼分析

InterProcessMutex類是curator中的排它鎖類,客戶端直接打交道的就是InterProcessMutex。所以我們從頂層開始,先分析InterProcessMutex。

 

實現接口

InterProcessMutex實現了兩個接口:

public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex>

InterProcessLock是分佈式鎖接口,分佈式鎖必須實現接口中的如下方法:

1、獲取鎖,直到鎖可用

public void acquire() throws Exception;

2、在指定等待的時間內獲取鎖。

public boolean acquire(long time, TimeUnit unit) throws Exception;

3、釋放鎖

public void release() throws Exception;

4、當前線程是否獲取了鎖

boolean isAcquiredInThisProcess();

以上方法也是InterProcessMutex暴露出來,供客戶端在使用分佈式鎖時調用。

Revocable<T>,實現該接口的鎖,鎖是可以被撤銷的。本編文章重點講解鎖的實現機制,關於撤銷部分不做討論。

 

屬性

InterProcessMutex屬性如下:

類型 名稱 說明
LockInternals internals 鎖的實現都在該類中,InterProcessMutex通過此類的方法實現鎖
String basePath 鎖節點在zk中的根路徑
ConcurrentMap<Thread, LockData> threadData 線程和自己的鎖相關數據映射
String LOCK_NAME 常量,值爲"lock-"。表示鎖節點的前綴

它還有一個內部靜態類LockData,也是threadData中保存的value,它定義了鎖的相關數據,包括鎖所屬線程,鎖的全路徑,和該線程加鎖的次數(InterProcessMutex爲可重入鎖)。代碼如下:

  1. private static class LockData
  2. {
  3. final Thread owningThread;
  4. final String lockPath;
  5. final AtomicInteger lockCount = new AtomicInteger(1);
  6. private LockData(Thread owningThread, String lockPath)
  7. {
  8. this.owningThread = owningThread;
  9. this.lockPath = lockPath;
  10. }
  11. }

 

構造方法

InterProcessMutex有三個構造方法,根據入參不同,嵌套調用,最終調用的構造方法如下:

  1. InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver)
  2. {
  3. basePath = PathUtils.validatePath(path);
  4. internals = new LockInternals(client, driver, path, lockName, maxLeases);
  5. }

可見構造方法最終初始化了兩個屬性,basePath被設置爲我們傳入的值 "/curator/lock",這是鎖的根節點。此外就是初始化了internals,前面說過internals是真正實現鎖功能的對象。真正幹活的是internals。

構造完InterProcessMutex對象後,我們看看它是如何工作的。

 

方法

InterProcessMutex實現InterProcessLock接口,關於分佈式鎖的幾個方法都在這個接口中,我們看看InterProcessMutex是如何實現的。

 

獲得鎖

獲得鎖有兩個方法,區別爲是否限定了等待鎖的時間長度。其實最終都是調用的私有方法internalLock()。不限定等待時長的代碼如下:

  1. public void acquire() throws Exception
  2. {
  3. if ( !internalLock(-1, null) )
  4. {
  5. throw new IOException("Lost connection while trying to acquire lock: " + basePath);
  6. }
  7. }

可以看到internalLock()返回false時,只可能因爲連接超時,否則會一直等待獲取鎖。

internalLock邏輯如下:

  1. 取得當前線程在threadData中的lockData
  2. 如果存在該線程的鎖數據,說明是鎖重入, lockData.lockCount加1,直接返回true。獲取鎖成功
  3. 如果不存在該線程的鎖數據,則通過internals.attemptLock()獲取鎖,此時線程被阻塞,直至獲得到鎖
  4. 鎖獲取成功後,把鎖的信息保存到threadData中。
  5. 如果沒能獲取到鎖,則返回false。

完整代碼如下:

  1. private boolean internalLock(long time, TimeUnit unit) throws Exception
  2. {
  3. /*
  4. Note on concurrency: a given lockData instance
  5. can be only acted on by a single thread so locking isn't necessary
  6. */
  7. Thread currentThread = Thread.currentThread();
  8. LockData lockData = threadData.get(currentThread);
  9. if ( lockData != null )
  10. {
  11. // re-entering
  12. lockData.lockCount.incrementAndGet();
  13. return true;
  14. }
  15. String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
  16. if ( lockPath != null )
  17. {
  18. LockData newLockData = new LockData(currentThread, lockPath);
  19. threadData.put(currentThread, newLockData);
  20. return true;
  21. }
  22. return false;
  23. }

可以看到獲取鎖的核心代碼是internals.attemptLock

 

釋放鎖

釋放鎖的方法爲release(),邏輯如下:

從threadData中取得當前線程的鎖數據,有如下情況:

  1. 不存在,拋出無此鎖的異常
  2. 存在,而且lockCount-1後大於零,說明該線程鎖重入了,所以直接返回,並不在zk中釋放。
  3. 存在,而且lockCount-1後小於零,說明有某種異常發生,直接拋異常
  4. 存在,而且lockCount-1等於零,這是無重入的正確狀態,需要做的就是從zk中刪除臨時節點,通過internals.releaseLock(),不管結果如何,在threadData中移除該線程的數據。

 

InterProcessMutex小結

分佈式鎖主要用到的是上面兩個方法,InterProcessMutex還有些其他的方法,這裏就不做具體講解,可以自己看一下,實現都不復雜。

通過對InterProcessMutex的講解,相信我們已經對鎖的獲得和釋放有了瞭解,應該也意識到真正實現鎖的是LockInternals類。接下來我們將重點講解LockInternals。

 

LockInternals源碼分析

Curator通過zk實現分佈式鎖的核心邏輯都在LockInternals中,我們按獲取鎖到釋放鎖的流程爲指引,逐步分析LockInternals的源代碼。

 

獲取鎖

InterProcessMutex獲取鎖的代碼分析中,可以看到它是通過internals.attemptLock(time, unit, getLockNodeBytes());來獲取鎖的,那麼我們就以這個方法爲入口。此方法的邏輯比較簡單,如下:

  1. 通過driver在zk上創建鎖節點,獲得鎖節點路徑。
  2. 通過internalLockLoop()方法阻塞進程,直到獲取鎖成功。

核心代碼如下:

  1. ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
  2. hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);

我們繼續分析internalLockLoop方法,獲取鎖的核心邏輯在此方法中。

internalLockLoop中通過while自旋,判斷鎖如果沒有被獲取,將不斷的去嘗試獲取鎖。

while循環中邏輯如下:

  1. 通過driver查看當前鎖節點序號是否排在第一位,如果排在第一位,說明取鎖成功,跳出循環
  2. 如果沒有排在第一位,則監聽自己的前序鎖節點,然後阻塞線程。

當前序節點釋放了鎖,監聽會被觸發,恢復線程,此時主線程又回到while中第一步。

重複以上邏輯,直至獲取到鎖(自己鎖的序號排在首位)。

internalLockLoop方法核心代碼如下:

  1. while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )
  2. {
  3. List<String> children = getSortedChildren();
  4. String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
  5. PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
  6. if ( predicateResults.getsTheLock() )
  7. {
  8. haveTheLock = true;
  9. }
  10. else
  11. {
  12. String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
  13. synchronized(this)
  14. {
  15. try
  16. {
  17. // use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak
  18. client.getData().usingWatcher(watcher).forPath(previousSequencePath);
  19. if ( millisToWait != null )
  20. {
  21. millisToWait -= (System.currentTimeMillis() - startMillis);
  22. startMillis = System.currentTimeMillis();
  23. if ( millisToWait <= 0 )
  24. {
  25. doDelete = true; // timed out - delete our node
  26. break;
  27. }
  28. wait(millisToWait);
  29. }
  30. else
  31. {
  32. wait();
  33. }
  34. }
  35. catch ( KeeperException.NoNodeException e )
  36. {
  37. // it has been deleted (i.e. lock released). Try to acquire again
  38. }
  39. }
  40. }
  41. }

獲取鎖的主要代碼邏輯我們到這就已經分析完了,可見和我們自己的實現還是基本一樣的。此外上面提到了driver對象,也就是StandardLockInternalsDriver類,它提供了一些輔助的方法,比如說在zk創建鎖節點,判斷zk上鎖序列第一位是否爲當前鎖,鎖序列的排序邏輯等。我們就不具體講解了。

 

釋放鎖

釋放鎖的邏輯很簡單,移除watcher,刪除鎖節點。代碼如下:

  1. final void releaseLock(String lockPath) throws Exception
  2. {
  3. client.removeWatchers();
  4. revocable.set(null);
  5. deleteOurPath(lockPath);
  6. }

 

總結

至此,Curator中InterProcessMutex的源代碼分析全部完成。簡單回顧下,InterProcessMutex類封裝上層邏輯,對外暴露鎖的使用方法。而真正的鎖實現邏輯在LockInternals中,它通過對zk臨時有序鎖節點的創建和監控,判斷自己的鎖序號是否在首位,來實現鎖的獲取。此外它還結合StandardLockInternalsDriver提供的方法,共同實現了排他鎖。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章