1. 程式人生 > 程式設計 >分散式鎖都有哪些實現方案?

分散式鎖都有哪些實現方案?

一、業務場景

同一個jvm裡多個執行緒操作同一個有狀態的變數,可以通過JVM內的鎖保證執行緒安全。

如果是多個JVM操作同一個有狀態的變數,如何保證執行緒安全呢?

這時候就需要分散式鎖來發揮它的作用了

二、特點

分散式系統往往業務流量比較大、併發較高,對分散式鎖的高可用和高效能有較高的要求。一般分散式鎖的方案需要滿足如下要求:

  • 有高可用的獲取鎖和釋放鎖功能
  • 獲取鎖和釋放鎖的效能要好
  • 這把鎖要是一把可重入鎖(避免死鎖)
  • 這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
  • 這把鎖最好是一把公平鎖(根據業務需求考慮要不要這條)

三、基於資料庫的分散式鎖方案

1、基於表主鍵唯一做分散式鎖

利用主鍵唯一的特性,如果有多個請求同時提交到資料庫的話,資料庫會保證只有一個插入操作可以成功,那麼我們就可以認為操作成功的那個執行緒獲得了該方法的鎖,當方法執行完畢之後,想要釋放鎖的話,刪除這條資料庫記錄即可

1.1、缺點

  • 資料庫單點
  • 沒有鎖超時機制
  • 不可重入
  • 非公平鎖
  • 非阻塞鎖

1.2、優化點

  • 資料庫主從備份,解決單點問題。因為主從同步有延遲,可能導致資料不一致
  • 定時任務檢測鎖超時自動釋放或者通過connection.commit()操作來釋放鎖
  • 加鎖加上機器和執行緒資訊,加鎖之前先查詢,支援可重入
  • 中間表,記錄加鎖失敗的機器執行緒,按照建立時間排序
  • 自旋實現阻塞效果

1.3、原理

一般資料庫使用innodb儲存引擎,在插入資料的時候會加行級鎖。從而達到是併發請求按順序執行的效果

2、通過資料庫mvcc實現樂觀鎖

更新資料的時候帶上指定版本號,如果被其他執行緒提前更新的版本號,則此次更新失敗

2.1、缺點

對資料庫表侵入較大,每個表需要增加version欄位

高併發下存在很多更新失敗

3、資料庫的限制

  • 使用排他鎖來進行分散式鎖的 lock,那麼一個排他鎖長時間不提交,就會佔用資料庫連線。一旦類似的連線變得多了,就可能把資料庫連線池撐爆。
  • 資料庫寫入是磁碟io,效能方面差一些
  • 資料庫能支援的最大qps也有限制,很難滿足高併發的需要

四、基於redis實現分散式鎖

1、原理

1.1、加鎖

原子命令:SET key value NX PX milliseconds

PX milliseconds 過期時間,防止加鎖執行緒死掉不能解鎖。過期時間設定太短,可能加鎖執行緒還沒有執行完正常邏輯,就到了過期時間

NX 如果沒有這個key則設定,存在key返回失敗

value 隨機值(一般用UUID),用來實現只能由加鎖執行緒解鎖

1.2、解鎖

lua指令碼實現get value,delete的操作。加鎖的時候設定的value是不會重複的隨機值,解鎖的時候必須UUID一致才能解鎖

2、缺點

  • 獲取鎖是非阻塞
  • 非公平鎖,不支援需要公平鎖的場景
  • redis主從存在延遲,在master宕機發生主從切換時,可能會導致鎖失效

五、基於Redlock演演算法實現分散式鎖。redisson對Redlock演演算法進行了封裝

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.3.2</version>
</dependency>
複製程式碼

1、原理

在Redis的分散式環境中,我們假設有N個Redis master。這些節點完全互相獨立,不存在主從複製或者其他叢集協調機制。我們確保將在N個例項上使用與在Redis單例項下相同方法獲取和釋放鎖。現在我們假設有5個Redis master節點,同時我們需要在5臺伺服器上面執行這些Redis例項,這樣保證他們不會同時都宕掉。

1.1、加鎖

假設有cluster-1,cluster-2,cluster-3總計3個cluster模式叢集。如果要獲取分散式鎖,那麼需要向這3個cluster叢集通過EVAL命令執行LUA指令碼,需要3/2+1=2,即至少2個cluster叢集響應成功。set的value要具有唯一性,redisson的value通過UUID+threadId保證value的唯一性

1.獲取當前時間(單位是毫秒)。

2.輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裡,客戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的超時時間可能是5-50毫秒的範圍,這個可以防止一個客戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該儘快嘗試下一個master節點。

3.客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是獲取成功了。

4.如果鎖獲取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前獲取鎖所消耗的時間。

5.如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有獲取成功的鎖。

1.2、釋放鎖

需要在所有節點都釋放鎖就行,不管之前有沒有在該節點獲取鎖成功。

客戶端如果沒有在多數節點獲取到鎖,一定要儘快在獲取鎖成功的節點上釋放鎖,這樣就沒必要等到key超時後才能重新獲取這個鎖

2、安全性論證

開始之前,讓我們假設客戶端可以在大多數節點都獲取到鎖,這樣所有的節點都會包含一個有相同存活時間的key。但是需要注意的是,這個key是在不同時間點設定的,所以這些key也會在不同的時間超時,但是我們假設最壞情況下第一個key是在T1時間設定的(客戶端連線到第一個伺服器時的時間),最後一個key是在T2時間設定的(客戶端收到最後一個伺服器返回結果的時間),從T2時間開始,我們可以確認最早超時的key至少也會存在的時間為MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT,TTL是鎖超時時間、(T2-T1)是最晚獲取到的鎖的耗時,CLOCK_DRIFT是不同程式間時鐘差異,這個是用來補償前面的(T2-T1)。其他的key都會在這個時間點之後才會超時,所以我們可以確定這些key在這個時間點之前至少都是同時存在的。

如果一個客戶端獲取大多數節點鎖的耗時接近甚至超過鎖的最大有效時間時(就是我們為SET操作設定的TTL值),那麼系統會認為這個鎖是無效的同時會釋放這些節點上的鎖,所以我們僅僅需要考慮獲取大多數節點鎖的耗時小於有效時間的情況。在這種情況下,根據我們前面的證明,在MIN_VALIDITY時間內,沒有客戶端能重新獲取鎖成功,所以多個客戶端都能同時成功獲取鎖的結果,只會發生在多數節點獲取鎖的時間都大大超過TTL時間的情況下,實際上這種情況下這些鎖都會失效

六、基於zookeeper實現分散式鎖

1、基本排他鎖(非公平鎖)

1.1、原理

利用臨時節點與 watch 機制。每個鎖佔用一個普通節點 /lock,當需要獲取鎖時在 /lock 目錄下建立一個臨時節點,建立成功則表示獲取鎖成功,失敗則 watch/lock 節點,有刪除操作後再去爭鎖。臨時節點好處在於當程式掛掉後能自動上鎖的節點自動刪除即取消鎖。

1.2、缺點

所有取鎖失敗的程式都監聽父節點,很容易發生羊群效應,即當釋放鎖後所有等待程式一起來建立節點,併發量很大。

2、優化後的排他鎖(公平鎖)

2.1、原理

上鎖改為建立臨時有序節點,每個上鎖的節點均能建立節點成功,只是其序號不同。只有序號最小的可以擁有鎖,如果這個節點序號不是最小的則 watch 序號比本身小的前一個節點 (公平鎖)。

3、共享鎖

3.1、原理

在鎖節點下建立臨時順序節點。讀節點為R+序號,寫節點為W+序號。建立完節點後,獲取所有子節點,對鎖節點註冊子節點變更的watcher監聽,確定自己的序號在所有子節點中的位置。對於讀請求,沒有比自己序號小的寫節點,就表示獲得了共享鎖,執行讀取邏輯。對於寫請求,如果自己不是序號最小的子節點,就需要進入等待。接收到watcher通知後,重複獲取鎖。

3.2、缺點

共享鎖羊群效應。大量的watcher通知和子節點列表獲取,兩個操作重複執行。叢集規模比較大的情況下,會對zookeeper伺服器造成巨大的效能影響和網路衝擊

3.3、優化

讀請求,監聽比自己小的寫節點。寫請求,監聽比自己小的最後一個節點。

4、zookeeper侷限

  • 效能上可能並沒有快取服務那麼高,因為每次在建立鎖和釋放鎖的過程中,都要動態建立、銷燬臨時節點來實現鎖功能。
  • ZK 中建立和刪除節點只能通過 Leader 伺服器來執行,然後將資料同步到所有的 Follower 機器上。
  • 併發度支援不如redis