分散式鎖的一些理解

曉乎發表於2020-06-10

 在多執行緒併發的情況下,單個節點內的執行緒安全可以通過synchronized關鍵字和Lock介面來保證。

synchronized和lock的區別

  1. Lock是一個介面,是基於在語言層面實現的鎖,而synchronized是Java中的關鍵字,是基於JVM實現的內建鎖,Java中的每一個物件都可以使用synchronized新增鎖。

  2. synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

  3. Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;

  4. Lock可以提高多個執行緒進行讀操作的效率。(可以通過readwritelock實現讀寫分離,一個用來獲取讀鎖,一個用來獲取寫鎖。)

  當開發的應用程式處於一個分散式的叢集環境中,涉及到多節點,多程式共同完成時,如何保證執行緒的執行順序是正確的。比如在高併發的情況下,很多企業都會使用Nginx反向代理伺服器實現負載均衡的目的,這個時候很多請求會被分配到不同的Server上,一旦這些請求涉及到對統一資源進行修改操作時,就會出現問題,這個時候在分散式系統中就需要一個全域性鎖實現多個執行緒(不同程式中的執行緒)之間的同步。

  常見的處理辦法有三種:資料庫、快取、分散式協調系統。資料庫和快取是比較常用的,但是分散式協調系統是不常用的。

  常用的分散式鎖的實現包含:

      Redis分散式鎖Zookeeper分散式鎖Memcached

基於 Redis 做分散式鎖

 Redis提供的三種方法:

(1)鎖 SETNX:只在鍵 key 不存在的情況下, 將鍵 key 的值設定為 value 。若鍵 key 已經存在, 則 SETNX 命令不做任何動作。SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。命令在設定成功時返回 1 , 設定失敗時返回 0

redis> SETNX job "programmer"    # job 設定成功
(integer) 1

redis> SETNX job "code-farmer"   # 嘗試覆蓋 job ,失敗

(2)解鎖 DEL:刪除給定的一個或多個 key

(3)鎖超時 EXPIRE: 為給定 key 設定生存時間,當 key 過期時(生存時間為 0 ),它會被自動刪除。

  每次當一個節點想要去操作臨界資源的時候,我們可以通過redis來的鍵值對來標記一把鎖,每一程式首先通過Redis訪問同一個key,對於每一個程式來說,如果該key不存在,則該執行緒可以獲取鎖,將該鍵值對寫入redis,如果存在,則說明鎖已經被其他程式所佔用。具體邏輯的虛擬碼如下:

try{
	if(SETNX(key, 1) == 1){
		//do something ......
	}finally{
	DEL(key);
}

  但是此時,又會出現問題,因為SETNX和DEL操作並不是原子操作,如果程式在執行完SETNX後,而並沒有執行EXPIRE就已經當機了,這樣一來,原先的問題依然存在,整個系統都將被阻塞。

  幸虧Redis又提供了SET key value timeout NX方法,可以以原子操作的方式完成SETNX和EXPIRE的操作。此時只需如下操作即可。

try{
	if(SET(key, 1, 30, timeout, NX) == 1){
		//do something ......
	}
}finally{
	DEL(key);
}

  解決了原子操作,仍然還有一點需要注意,例如,A節點的程式獲取到鎖的時候,A程式可能執行的很慢,在do something未完成的情況下,30秒的時間片已經使用完,此時會將該key給深處掉,此時B程式發現這個key不存在,則去訪問,併成功的獲取到鎖,開始執行do something,此時A執行緒恰好執行到DEL(key),會將B的key刪除掉,此時相當於B執行緒在訪問沒有加鎖的臨界資源,而其餘程式都有機會同時去操作這個臨界資源,會造成一些錯誤的結果。對於該問題的解決辦法是程式在刪除key之前可以做一個判斷,驗證當前的鎖是不是本程式加的鎖。

String threadId = Thread.currentThread().getId()
try{
	if(SET(key, threadId, 30, timeout, NX) == 1){
		//do something ......
	}
}finally{
    if(threadId.equals(redisClient.get(key))){
        DEL(key);
    }
}

   上面的改進雖然解決鎖被不同的程式釋放的危險,但並沒有解決獲取到鎖的程式在指定的時間內未完成do something操作(上面的程式碼還有一點小問題,就是判斷操作和釋放鎖是兩個獨立的操作,不具備原子性。假設執行緒A判斷完確實是自己加的鎖 , 這時還沒del ,這時有效的時間用完了 , 緊接著執行緒B又馬上搶到了鎖 , 然後執行緒A才執行del命令 , 就會把B搶到的鎖給誤刪了),使得卡住的程式有可能與後來的程式同時同問臨界資源,而出現問題,因此一旦某個程式無法在超時時間內完成對臨界資源的操作,就需要延長超時的時間。此時可以啟動一個守護程式,監視指定時間內獲取鎖的程式是否完成操作,如果沒有,則新增超時時間,讓程式繼續執行。

String threadId = Thread.currentThread().getId()
try{
	if(SET(key, threadId, 30, timeout, NX) == 1){
		new Thread(){
            @Override
            public void run() {
            	//start Daemon
            }
         }
		//do something ......
	}
}finally{
    if(threadId.equals(redisClient.get(key))){
        DEL(key);
    }
}

  基於以上的分析,基本上可以通過Redis實現一個分散式鎖,如果我們想提升該分散式的效能,我們可以對連線資源進行分段處理,將請求均勻的分佈到這些臨界資源段中,比如一個買票系統,我們可以將100張票分為10 部分,每部分包含10張票放在其他的服務節點上,這些請求可以通過Nginx被均勻的分散到這些處理節點上,可以加快對臨界資源的處理。

參考資料

  1. 併發程式設計的鎖機制:synchronized和lock

  2. B站視訊上一部分講解

  3. 什麼是分散式鎖?

相關文章