前言 最近懶成一坨屎,學不動系列一波接一波,大多還都是底層原理相關的。上周末抽時間重讀了周志明大濕的 JVM 高效併發部分,每讀一遍都有不同的感悟。路漫漫,藉此,把前段時間搞著玩的秒殺案例中的分散式鎖深入瞭解一下。 案例介紹 在嘗試瞭解分散式鎖之前,大家可以想象一下,什麼場景下會使用分散式鎖? 單機 ...
前言
最近懶成一坨屎,學不動系列一波接一波,大多還都是底層原理相關的。上周末抽時間重讀了周志明大濕的 JVM 高效併發部分,每讀一遍都有不同的感悟。路漫漫,藉此,把前段時間搞著玩的秒殺案例中的分散式鎖深入瞭解一下。
案例介紹
在嘗試瞭解分散式鎖之前,大家可以想象一下,什麼場景下會使用分散式鎖?
單機應用架構中,秒殺案例使用ReentrantLcok或者synchronized來達到秒殺商品互斥的目的。然而在分散式系統中,會存在多台機器並行去實現同一個功能。也就是說,在多進程中,如果還使用以上JDK提供的進程鎖,來併發訪問資料庫資源就可能會出現商品超賣的情況。因此,需要我們來實現自己的分散式鎖。
實現一個分散式鎖應該具備的特性:
高可用、高性能的獲取鎖與釋放鎖
在分散式系統環境下,一個方法或者變數同一時間只能被一個線程操作
具備鎖失效機制,網路中斷或宕機無法釋放鎖時,鎖必須被刪除,防止死鎖
具備阻塞鎖特性,即沒有獲取到鎖,則繼續等待獲取鎖
具備非阻塞鎖特性,即沒有獲取到鎖,則直接返回獲取鎖失敗
具備可重入特性,一個線程中可以多次獲取同一把鎖,比如一個線程在執行一個帶鎖的方法,該方法中又調用了另一個需要相同鎖的方法,則該線程可以直接執行調用的方法,而無需重新獲得鎖
在之前的秒殺案例中,我們曾介紹過關於分散式鎖幾種實現方式:
- 基於資料庫實現分散式鎖
- 基於 Redis 實現分散式鎖
- 基於 Zookeeper 實現分散式鎖
前兩種對於分散式生產環境來說並不是特別推薦,高併發下資料庫鎖性能太差,Redis在鎖時間限制和緩存一致性存在一定問題。這裡我們重點介紹一下 Zookeeper 如何實現分散式鎖。
實現原理
ZooKeeper是一個分散式的,開放源碼的分散式應用程式協調服務,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能存在唯一文件名。
數據模型
PERSISTENT 持久化節點,節點創建後,不會因為會話失效而消失
EPHEMERAL 臨時節點, 客戶端session超時此類節點就會被自動刪除
EPHEMERAL_SEQUENTIAL 臨時自動編號節點
PERSISTENT_SEQUENTIAL 順序自動編號持久化節點,這種節點會根據當前已存在的節點數自動加 1
監視器(watcher)
當創建一個節點時,可以註冊一個該節點的監視器,當節點狀態發生改變時,watch被觸發時,ZooKeeper將會向客戶端發送且僅發送一條通知,因為watch只能被觸發一次。
根據zookeeper的這些特性,我們來看看如何利用這些特性來實現分散式鎖:
創建一個鎖目錄lock
線程A獲取鎖會在lock目錄下,創建臨時順序節點
獲取鎖目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖
線程B創建臨時節點並獲取所有兄弟節點,判斷自己不是最小節點,設置監聽(watcher)比自己次小的節點(只關註比自己次小的節點是為了防止發生“羊群效應”)
線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是最小的節點,獲得鎖
代碼分析
儘管ZooKeeper已經封裝好複雜易出錯的關鍵服務,將簡單易用的介面和性能高效、功能穩定的系統提供給用戶。但是如果讓一個普通開發者去手擼一個分散式鎖還是比較困難的,在秒殺案例中我們直接使用 Apache 開源的curator 開實現 Zookeeper 分散式鎖。
這裡我們使用以下版本,截止目前最新版4.0.1:
<!-- zookeeper 分散式鎖、註意zookeeper版本 這裡對應的是3.4.6-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.10.0</version>
</dependency>
首先,我們看下InterProcessLock介面中的幾個方法:
/**
* 獲取鎖、阻塞等待、可重入
*/
public void acquire() throws Exception;
/**
* 獲取鎖、阻塞等待、可重入、超時則獲取失敗
*/
public boolean acquire(long time, TimeUnit unit) throws Exception;
/**
* 釋放鎖
*/
public void release() throws Exception;
/**
* Returns true if the mutex is acquired by a thread in this JVM
*/
boolean isAcquiredInThisProcess();
獲取鎖:
//獲取鎖
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
/*
實現同一個線程可重入性,如果當前線程已經獲得鎖,
則增加鎖數據中lockCount的數量(重入次數),直接返回成功
*/
//獲取當前線程
Thread currentThread = Thread.currentThread();
//獲取當前線程重入鎖相關數據
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
//原子遞增一個當前值,記錄重入次數,後面鎖釋放會用到
lockData.lockCount.incrementAndGet();
return true;
}
//嘗試連接zookeeper獲取鎖
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
//創建可重入鎖數據,用於記錄當前線程重入次數
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
//獲取鎖超時或者zk通信異常返回失敗
return false;
}
Zookeeper獲取鎖實現:
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
//獲取當前時間戳
final long startMillis = System.currentTimeMillis();
//如果unit不為空(非阻塞鎖),把當前傳入time轉為毫秒
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
//子節點標識
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
//嘗試次數
int retryCount = 0;
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
//自旋鎖,迴圈獲取鎖
while ( !isDone )
{
isDone = true;
try
{
//在鎖節點下創建臨時且有序的子節點,例如:_c_008c1b07-d577-4e5f-8699-8f0f98a013b4-lock-000000001
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
//如果當前子節點序號最小,獲得鎖則直接返回,否則阻塞等待前一個子節點刪除通知(release釋放鎖)
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e )
{
//異常處理,如果找不到節點,這可能發生在session過期等時,因此,如果重試允許,只需重試一次即可
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
isDone = false;
}
else
{
throw e;
}
}
}
//如果獲取鎖則返回當前鎖子節點路徑
if ( hasTheLock )
{
return ourPath;
}
return null;
}
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
boolean haveTheLock = false;
boolean doDelete = false;
try
{
if ( revocable.get() != null )
{
client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}
//自旋獲取鎖
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();
synchronized(this)
{
try
{
//這裡使用getData()介面而不是checkExists()是因為,如果前一個子節點已經被刪除了那麼會拋出異常而且不會設置事件監聽器,而checkExists雖然也可以獲取到節點是否存在的信息但是同時設置了監聽器,這個監聽器其實永遠不會觸發,對於Zookeeper來說屬於資源泄露
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
//如果設置了獲取鎖等待時間
if ( millisToWait <= 0 )
{
doDelete = true; // 超時則刪除子節點
break;
}
//等待超時時間
wait(millisToWait);
}
else
{
wait();//一直等待
}
}
catch ( KeeperException.NoNodeException e )
{
// it has been deleted (i.e. lock released). Try to acquire again
//如果前一個子節點已經被刪除則deException,只需要自旋獲取一次即可
}
}
}
}
}
catch ( Exception e )
{
ThreadUtils.checkInterrupted(e);
doDelete = true;
throw e;
}
finally
{
if ( doDelete )
{
deleteOurPath(ourPath);//獲取鎖超時則刪除節點
}
}
return haveTheLock;
}
釋放鎖:
public void release() throws Exception
{
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
//沒有獲取鎖,你釋放個球球,如果為空拋出異常
if ( lockData == null )
{
throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
}
//獲取重入數量
int newLockCount = lockData.lockCount.decrementAndGet();
//如果重入鎖次數大於0,直接返回
if ( newLockCount > 0 )
{
return;
}
//如果重入鎖次數小於0,拋出異常
if ( newLockCount < 0 )
{
throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
}
try
{
//釋放鎖
internals.releaseLock(lockData.lockPath);
}
finally
{