鎖和分佈式鎖的一些理解

一、鎖

1.自旋鎖

自旋鎖是一種非阻塞鎖,也就是說,如果某線程需要獲取自旋鎖,但該鎖已經被其他線程佔用時,該線程不會被掛起,而是在不斷的消耗CPU的時間,不停的試圖獲取自旋鎖

2.互斥鎖

互斥鎖是阻塞鎖,當某線程無法獲取互斥鎖時,該線程會被直接掛起,不再消耗CPU時間,當其他線程釋放互斥鎖後,操作系統會喚醒那個被掛起的線程。

阻塞鎖可以說是讓線程進入阻塞狀態進行等待,當獲得相應的信號(喚醒,時間)時,纔可以進入線程的準備就緒狀態,準備就緒狀態的所有線程,通過競爭進入運行狀態。它的優勢在於,阻塞的線程不會佔用 CPU 時間, 不會導致 CPU 佔用率過高,但進入時間以及恢復時間都要比自旋鎖略慢。在競爭激烈的情況下阻塞鎖的性能要明顯高於自旋鎖。

JAVA中,能夠進入/退出、阻塞狀態或包含阻塞鎖的方法有:
synchronized
ReentrantLock
Object.wait()/notify()
LockSupport.park()/unpart()(j.u.c經常使用)
自旋鎖 vs 互斥鎖
多核情況下,纔會使用到自旋鎖。
在多核情況下,如果線程切換的時間比線程等待的時間大,那麼就用自旋鎖。

3.可重入鎖
sysnchronized 和 reentrantlock 都是可重入的。

二、分佈式鎖

1、數據庫的分佈式鎖

樂觀鎖的version比較

2. redis實現的分佈式鎖

a. 基於setnx、expire兩個命令來實現

基於setnx(set if not exist)的特點,當緩存裏key不存在時,纔會去set,否則直接返回false。如果返回true則獲取到鎖,否則獲取鎖失敗,爲了防止死鎖,我們再用expire命令對這個key設置一個超時時間來避免。但是這裏看似完美,實則有缺陷,當我們setnx成功後,線程發生異常中斷,expire還沒來的及設置,那麼就會產生死鎖。

解決上述問題有兩種方案:

第一種是採用Redis 2.6.12版本以後的set,它提供了一系列選項

  • EX seconds – 設置鍵key的過期時間,單位時秒
  • PX milliseconds – 設置鍵key的過期時間,單位時毫秒
  • NX – 只有鍵key不存在的時候纔會設置key的值
  • XX – 只有鍵key存在的時候纔會設置key的值

第二種採用setnx(),get(),getset()實現,大體的實現過程如下:

(1) 線程Asetnx,值爲超時的時間戳(t1),如果返回true,獲得鎖。

(2) 線程B用get 命令獲取t1,與當前時間戳比較,判斷是否超時,沒超時false,如果已超時執行步驟3

(3) 計算新的超時時間t2,使用getset命令返回t3(這個值可能其他線程已經修改過),如果t1==t3,獲得鎖,如果t1!=t3說明鎖被其他線程獲取了

(4) 獲取鎖後,處理完業務邏輯,再去判斷鎖是否超時,如果沒超時刪除鎖,如果已超時,不用處理(防止刪除其他線程的鎖)

b. RedLock算法

RedLock算法是Redis作者推薦的一種分佈式鎖實現方式,算法的內容如下:

(1) 獲取當前時間;

(2) 嘗試從5個相互獨立Redis客戶端獲取鎖;

(3) 計算獲取所有鎖消耗的時間,當且僅當客戶端從多數節點獲取鎖,並且獲取鎖的時間小於鎖的有效時間,認爲獲得鎖;

(4) 重新計算有效期時間,原有效時間減去獲取鎖消耗的時間;

(5) 刪除所有實例的鎖

RedLock算法相對於單節點Redis鎖可靠性要更高,但是實現起來條件也較爲苛刻。

(1) 必須部署5個節點才能讓RedLock的可靠性更強。

(2) 需要請求5個節點才能獲取到鎖,通過Future的方式,先併發向5個節點請求,再一起獲得響應結果,能縮短響應時間,不過還是比單節點Redis鎖要耗費更多時間。

然後由於必須獲取到5個節點中的3個以上,所以可能出現獲取鎖衝突,即大家都獲得了1-2把鎖,結果誰也不能獲取到鎖,這個問題,Redis作者借鑑了Raft算法的精髓,通過沖突後在隨機時間開始,可以大大降低衝突時間,但是這問題並不能很好的避免,特別是在第一次獲取鎖的時候,所以獲取鎖的時間成本增加了。

如果5個節點有2個宕機,此時鎖的可用性會極大降低,首先必須等待這兩個宕機節點的結果超時才能返回,另外只有3個節點,客戶端必須獲取到這全部3個節點的鎖才能擁有鎖,難度也加大了。

如果出現網絡分區,那麼可能出現客戶端永遠也無法獲取鎖的情況,介於這種情況,下面我們來看一種更可靠的分佈式鎖ZooKeeper鎖。

3.ZooKeeper分佈式鎖

首先我們來了解一下ZooKeeper的特性,看看它爲什麼適合做分佈式鎖,ZooKeeper是一個爲分佈式應用提供一致性服務的軟件,它內部是一個分層的文件系統目錄樹結構,規定統一個目錄下只能有一個唯一文件名。

數據模型:

  • 永久節點:節點創建後,不會因爲會話失效而消失
  • 臨時節點:與永久節點相反,如果客戶端連接失效,則立即刪除節點
  • 順序節點:與上述兩個節點特性類似,如果指定創建這類節點時,zk會自動在節點名後加一個數字後綴,並且是有序的。

監視器(watcher):

  • 當創建一個節點時,可以註冊一個該節點的監視器,當節點狀態發生改變時,watch被觸發時,ZooKeeper將會向客戶端發送且僅發送一條通知,因爲watch只能被觸發一次。

根據ZooKeeper的這些特性,我們來看看如何利用這些特性來實現分佈式鎖:

  1. 創建一個鎖目錄lock

  2. 希望獲得鎖的線程A就在lock目錄下,創建臨時順序節點

  3. 獲取鎖目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖

  4. 線程B獲取所有節點,判斷自己不是最小節點,設置監聽(watcher)比自己次小的節點(只關注比自己次小的節點是爲了防止發生“羊羣效應”)

  5. 線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是最小的節點,獲得鎖。








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