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

1、業務場景

同一個jvm裏多個線程操做同一個有狀態的變量,能夠經過JVM內的鎖保證線程安全。redis

若是是多個JVM操做同一個有狀態的變量,如何保證線程安全呢?算法

這時候就須要分佈式鎖來發揮它的做用了數據庫

2、特色

分佈式系統每每業務流量比較大、併發較高,對分佈式鎖的高可用和高性能有較高的要求。通常分佈式鎖的方案須要知足以下要求:緩存

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

3、基於數據庫的分佈式鎖方案

一、基於表主鍵惟一作分佈式鎖

利用主鍵惟一的特性,若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個插入操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,當方法執行完畢以後,想要釋放鎖的話,刪除這條數據庫記錄便可安全

1.一、缺點

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

1.二、優化點

  • 數據庫主從備份,解決單點問題。由於主從同步有延遲,可能致使數據不一致
  • 定時任務檢測鎖超時自動釋放或者經過connection.commit()操做來釋放鎖
  • 加鎖加上機器和線程信息,加鎖以前先查詢,支持可重入
  • 中間表,記錄加鎖失敗的機器線程,按照建立時間排序
  • 自旋實現阻塞效果

1.三、原理

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

二、經過數據庫mvcc實現樂觀鎖

更新數據的時候帶上指定版本號,若是被其餘線程提早更新的版本號,則這次更新失敗服務器

2.一、缺點

對數據庫表侵入較大,每一個表須要增長version字段網絡

高併發下存在不少更新失敗併發

三、數據庫的限制

  • 使用排他鎖來進行分佈式鎖的 lock,那麼一個排他鎖長時間不提交,就會佔用數據庫鏈接。一旦相似的鏈接變得多了,就可能把數據庫鏈接池撐爆。
  • 數據庫寫入是磁盤io,性能方面差一些
  • 數據庫能支持的最大qps也有限制,很難知足高併發的須要

4、基於redis實現分佈式鎖

一、原理

1.一、加鎖

原子命令:SET key value NX PX millisecondsmvc

PX milliseconds 過時時間,防止加鎖線程死掉不能解鎖。過時時間設置過短,可能加鎖線程尚未執行完正常邏輯,就到了過時時間

NX 若是沒有這個key則設置,存在key返回失敗

value 隨機值(通常用UUID),用來實現只能由加鎖線程解鎖

1.二、解鎖

lua腳本實現get value,delete的操做。加鎖的時候設置的value是不會重複的隨機值,解鎖的時候必須UUID一致才能解鎖

二、缺點

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

5、基於Redlock算法實現分佈式鎖。redisson對Redlock算法進行了封裝

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

一、原理

在Redis的分佈式環境中,咱們假設有N個Redis master。這些節點徹底互相獨立,不存在主從複製或者其餘集羣協調機制。咱們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖。如今咱們假設有5個Redis master節點,同時咱們須要在5臺服務器上面運行這些Redis實例,這樣保證他們不會同時都宕掉。

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.二、釋放鎖

須要在全部節點都釋放鎖就行,無論以前有沒有在該節點獲取鎖成功。

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

二、安全性論證

開始以前,讓咱們假設客戶端能夠在大多數節點都獲取到鎖,這樣全部的節點都會包含一個有相同存活時間的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時間的狀況下,實際上這種狀況下這些鎖都會失效

6、基於zookeeper實現分佈式鎖

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

1.一、原理

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

1.二、缺點

全部取鎖失敗的進程都監聽父節點,很容易發生羊羣效應,即當釋放鎖後全部等待進程一塊兒來建立節點,併發量很大。

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

2.一、原理

上鎖改成建立臨時有序節點,每一個上鎖的節點均能建立節點成功,只是其序號不一樣。只有序號最小的能夠擁有鎖,若是這個節點序號不是最小的則 watch 序號比自己小的前一個節點 (公平鎖)。

三、共享鎖

3.一、原理

在鎖節點下建立臨時順序節點。讀節點爲R+序號,寫節點爲W+序號。建立完節點後,獲取全部子節點,對鎖節點註冊子節點變動的watcher監聽,肯定本身的序號在全部子節點中的位置。對於讀請求,沒有比本身序號小的寫節點,就表示得到了共享鎖,執行讀取邏輯。對於寫請求,若是本身不是序號最小的子節點,就須要進入等待。接收到watcher通知後,重複獲取鎖。

3.二、缺點

共享鎖羊羣效應。大量的watcher通知和子節點列表獲取,兩個操做重複運行。集羣規模比較大的狀況下,會對zookeeper服務器形成巨大的性能影響和網絡衝擊

3.三、優化

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

四、zookeeper侷限

  • 性能上可能並無緩存服務那麼高,由於每次在建立鎖和釋放鎖的過程當中,都要動態建立、銷燬臨時節點來實現鎖功能。
  • ZK 中建立和刪除節點只能經過 Leader 服務器來執行,而後將數據同步到全部的 Follower 機器上。
  • 併發度支持不如redis
相關文章
相關標籤/搜索