分佈式鎖的一些理解

 在多線程併發的狀況下,單個節點內的線程安全能夠經過synchronized關鍵字和Lock接口來保證。java

synchronized和lock的區別redis

  1. Lock是一個接口,是基於在語言層面實現的鎖,而synchronized是Java中的關鍵字,是基於JVM實現的內置鎖,Java中的每個對象均可以使用synchronized添加鎖。數據庫

  2. synchronized在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生;而Lock在發生異常時,若是沒有主動經過unLock()去釋放鎖,則極可能形成死鎖現象,所以使用Lock時須要在finally塊中釋放鎖;編程

  3. Lock可讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不可以響應中斷;緩存

  4. Lock能夠提升多個線程進行讀操做的效率。(能夠經過readwritelock實現讀寫分離,一個用來獲取讀鎖,一個用來獲取寫鎖。)安全

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

  常見的處理辦法有三種:數據庫、緩存、分佈式協調系統。數據庫和緩存是比較經常使用的,可是分佈式協調系統是不經常使用的。服務器

  經常使用的分佈式鎖的實現包含:多線程

      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. 什麼是分佈式鎖?

相關文章
相關標籤/搜索