1. 程式人生 > >【JAVA程式設計】--分散式鎖基礎

【JAVA程式設計】--分散式鎖基礎

1.實現分散式鎖需要考慮的幾點:

1.1執行緒和鎖關係

拿最常用的互斥鎖來說 
synchronized內建鎖是作用於物件,java中每個物件是唯一存在的, 
每個物件的物件頭中包含獲取該物件鎖的執行緒ID,那就保證了執行緒對該物件鎖的唯一性。 
ReentrantLock內含Sync物件,其繼承自同步器物件,同步器物件繼承自 AbstractOwnableSynchronizer, 
該物件可以設定獲取該鎖的獨佔執行緒。 
從上面兩種鎖可以看出,鎖的標識要與執行緒保持唯一性的關係 
不例外的分散式鎖也該如此,需要明確當前鎖被哪個執行緒佔有,也就是要維護鎖與執行緒的關係。

1.2.如何防止死鎖的產生

對於synchronized產生的死鎖,似乎我們無能為力,即死鎖狀態無法解除; 
一個執行緒已經獲得了物件鎖,其他執行緒訪問共享物件的時就必須無限期等待,不能中斷那些獲取鎖的線 程。 
因此我們編碼時讓執行緒按照相同的順序獲得一組鎖進行預防。 
而Lock提供了更加靈活的方法,如果當前鎖可用則返回true,否則返回false,並且可以設定獲取鎖的超時 時間,超時退出,防止死鎖。 
對於分散式的鎖第一要設定鎖的超時時間,讓鎖能及時釋放掉。 
其次還要設定客戶端請求鎖的超時時間,以防止通訊過程出現問題,客戶端執行緒一直等待鎖響應。

1.3.保證互斥性和重入性

哪一個執行緒可以獲得鎖(獨佔鎖)?哪些執行緒可以獲取鎖(共享鎖)?執行緒之間互斥鎖與共享鎖是如何保證的? 
我們先來看下傳統鎖是如何來處理的 
synchronized是互斥鎖,只需維護互斥關係,物件唯一,鎖唯一; 
重入性也是維護計數器,累加; 
Lock既有互斥鎖也有共享鎖 
其互斥特性是利用int變數state來判斷,當然state是用volatile修飾。 
判斷state是否為0,如果為0,設定當前執行緒為獨佔鎖擁有者,並將state加1; 
重入特性實現也比較簡單,state一直累加即可。 
redis如何保證互斥性 
使用過期時間,如果當前應用的執行緒獲取鎖的過期時間是null,設定鎖的過期時間,並返回null, 
當然該操作是原子操作(如何保證原子性,請看下文); 
如果在該鎖過期時間內,其他執行緒獲取的過期時間不為空,也就沒有獲得鎖。 
重入性:使用hset,設定當前鎖的value值,如果重複獲取鎖,則累加;釋放鎖遞減。

1.4.如何保證原子性

上面幾種實現方式中,存在的原子性的問題。那如何來保證操作的原子性? 
 貌似通過我們的java程式保證是無濟於事的; 
 redis在2.6版本以後,可以使用lua語言編寫指令碼傳到redis服務端執行,
 將我們之前多次與服務端互動才能完成的功能放到一次指令碼中處理, 
 那我們所擔心的原子性問題迎刃而解,還可以減少網路傳輸。

5.未獲取鎖的執行緒如何處理

得到鎖的執行緒如願以償的去執行臨界區內程式碼了,那未得到鎖的執行緒去哪裡了? 
有兩種方式: 
非阻塞:未獲取到鎖的執行緒一直迴圈看鎖的持有者是否釋放鎖。 
優點:處理相對簡單 缺點:佔用cpu資源,容易產生死鎖 
阻塞:未獲得鎖,將執行緒本身進行阻塞。 
優點:對執行緒統一管理排程 缺點:邏輯相對複雜 
Lock把未得到鎖的執行緒封裝成Node節點,放入其構造的虛擬雙向佇列中,該佇列是FIFO佇列, 
並進行阻塞操作,等待東山再起,;將佇列符合條件的執行緒呼叫park()方法掛起阻塞。 
當獲取鎖的執行緒釋放鎖時,會呼叫unpark()喚醒佇列中第一個阻塞節點,使程式在阻塞處繼續執行, 
讓佇列head節點的下一個節點持有的執行緒獲得鎖,並且將該節點設為head節點。過程見下圖: 

synchronized也是封裝Node節點,構造虛擬佇列,與Lock不同的是,該佇列是LIFO佇列,所有請求鎖的執行緒都被放入佇列中,將符合條 件的執行緒移到另外的佇列,具體過程不再贅述;也是採用阻塞的方式處理執行緒。

6.那分散式redis鎖該如何處理未獲得到鎖的執行緒呢?

採用非阻塞方式,沒得到鎖的執行緒,不斷的輪詢來獲取鎖,很明顯這樣的方式會增加額外的無用功,會增加redis服務端節點的壓力。 
如何進行優化? 
一個應用裡面可能會有多個執行緒競爭該鎖,可以控制應用裡執行緒對鎖的申請頻率 
1.讓執行緒sleep一段時間再請求鎖,那sleep多長時間呢,時間不好把控。 
2.單個應用程序裡可以使用訊號量來限制,那就需要對訊號量進行增減操作,來控制一定數量的執行緒。
沒有訊號量的執行緒阻塞,直到某個執行緒釋放鎖後訊號量加1,執行緒獲得訊號量後來競爭鎖。
那就需要監聽線 程釋放鎖的操作,如何監聽呢?可以使用redis的pub/sub方式,非同步處理,增加吞吐量; 
首先執行緒訂閱某個鎖的topic,如果獲取不到鎖,就發起sub操作,並阻塞當前執行緒,一旦有執行緒釋放鎖,pub訊息給訂閱的客戶端,
客戶端進行訊號量的處理,使阻塞的執行緒獲取許可來競爭鎖。對於某個鎖有哪些執行緒需要,也就轉移到redis服務端記錄。 
具體過程如下圖: 

在這裡插入圖片描述

2.redis的分散式鎖設計思路

1.概念
我們在開發時最常用的一個是java給我們提供的基於jvm的鎖,鎖的獲取和釋放由jvm來管理,我們只需要標註synchronized就可以。
另外還有Lock,需要顯式的呼叫鎖定和解鎖。這兩種鎖的作用範圍是一個jvm程序,也就是我們的一個系統中;
在分散式系統中,一個叢集內的不同主機或者不同叢集同時訪問共享的資源,會出現競態條件(兩個或多個執行緒競爭同一資源時,如果對資源 的訪問順序敏感就稱為競態條件),使用傳統的鎖就沒有辦法 處理,此時就需要使用分散式鎖來解決。
分散式鎖主要就是解決分散式系統中共享資源的競態條件問題。
2.使用場景
多個應用有操作共享資源的情景
3.需要具備的功能
先看下我們所熟悉的鎖都具備什麼功能

  1. 作為鎖所要具備的最基本的功能其一是獲取鎖,其二是釋放鎖;
    synchronized關鍵字在jvm中使用了位元組碼指令monitorenter和monitorexitlock來獲取和釋放物件鎖,
    這兩個位元組碼指令隱式的呼叫了lock和unlock操作。鎖的獲取和釋放無需我們關心,jvm程序掛掉資源回收;
    java提供的Lock需要我們顯示的呼叫鎖定和解鎖。底層使用cas演算法,控制原子變數的狀態,來標記鎖的獲取與釋放,
    同樣jvm程序掛掉資源進行回收;
    分散式鎖同樣也需要提供獲取和釋放鎖的功能。
  2. 處理死鎖
    當出現死鎖情況時最好能在一定時間內打破死鎖的狀態,否則會一直佔用鎖,佔用資源。
  3. 重入性
    一般鎖都會具備的特性,可重複獲取已經獲取到的鎖。
  4. 鎖的效能等
    鎖效能的好壞直接影響系統及程式的執行,如synchronized持續優化,鎖粒度及鎖升級策略都是為了獲得更好的效能。
    4.實現方式
    方式:incr、decr 原子操作
    加鎖:在需要使用的地方執行該key的incr操作,如果返回值是1,則獲取鎖
    解鎖:在finally塊中將key做decr操作
    設定過期時間:如果程序掛掉,導致鎖沒有釋放,自動過期刪除
    優點:操作簡單易行
    缺點:
    1.設定過期時間正確
    獲取鎖的客戶端程序在執行過程中掛掉,沒有走finally塊減一,那其他程序只能等redis的ttl自動刪除;
    該過期時間設定的長短難以把控,如果我們的請求因為其他原因阻塞了沒有處理完,但已經到了redis的過期時間,
    其他程序可以獲得鎖進行 處理,結果。。。
    2.設定過期時間不正確
    設定key的自增和設定過期時間不是原子操作,假如前者設定成功了,而過期時間因為各種原因沒有設定成功,
    一旦該鎖的計數出現錯誤,那麼所有程序都無法獲取到鎖,結果。。。
    總結上面方式所存在的問題:
    1.操作非原子性
    2.網路中斷、命令傳送失敗
    3.死鎖
    4.互斥