當前位置:首頁 » 數據倉庫 » 資料庫鎖分布式鎖
擴展閱讀
webinf下怎麼引入js 2023-08-31 21:54:13
堡壘機怎麼打開web 2023-08-31 21:54:11

資料庫鎖分布式鎖

發布時間: 2022-09-23 13:26:19

① 使用redis實現的分布式鎖原理是什麼

一、寫在前面

現在面試,一般都會聊聊分布式系統這塊的東西。通常面試官都會從服務框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事務、分布式鎖、ZooKeeper等知識。

所以咱們這篇文章就來聊聊分布式鎖這塊知識,具體的來看看Redis分布式鎖的實現原理。

說實話,如果在公司里落地生產環境用分布式鎖的時候,一定是會用開源類庫的,比如Redis分布式鎖,一般就是用Redisson框架就好了,非常的簡便易用。

大家如果有興趣,可以去看看Redisson的官網,看看如何在項目中引入Redisson的依賴,然後基於Redis實現分布式鎖的加鎖與釋放鎖。

下面給大家看一段簡單的使用代碼片段,先直觀的感受一下:

大家看到了吧,那個myLock的hash數據結構中的那個客戶端ID,就對應著加鎖的次數

(5)釋放鎖機制

如果執行lock.unlock(),就可以釋放分布式鎖,此時的業務邏輯也是非常簡單的。

其實說白了,就是每次都對myLock數據結構中的那個加鎖次數減1。

如果發現加鎖次數是0了,說明這個客戶端已經不再持有鎖了,此時就會用:

「del myLock」命令,從redis里刪除這個key。

然後呢,另外的客戶端2就可以嘗試完成加鎖了。

這就是所謂的分布式鎖的開源Redisson框架的實現機制。

一般我們在生產系統中,可以用Redisson框架提供的這個類庫來基於redis進行分布式鎖的加鎖與釋放鎖。

(6)上述Redis分布式鎖的缺點

其實上面那種方案最大的問題,就是如果你對某個redis master實例,寫入了myLock這種鎖key的value,此時會非同步復制給對應的master slave實例。

但是這個過程中一旦發生redis master宕機,主備切換,redis slave變為了redis master。

接著就會導致,客戶端2來嘗試加鎖的時候,在新的redis master上完成了加鎖,而客戶端1也以為自己成功加了鎖。

此時就會導致多個客戶端對一個分布式鎖完成了加鎖。

這時系統在業務語義上一定會出現問題,導致各種臟數據的產生。

所以這個就是redis cluster,或者是redis master-slave架構的主從非同步復制導致的redis分布式鎖的最大缺陷:在redis master實例宕機的時候,可能導致多個客戶端同時完成加鎖。

② 分布式鎖實現及常見問題

對於平台系統,由於系統是集群模式,每個節點都是無狀態模式,提供統一的功能,所以無法使用JVM的鎖,需要使用分布式鎖保證所有機器只有一個在執行

由於分布式鎖一般是通過其他機器或進程(redis,zk,mysql等)來記錄狀態所以需要有的特性

Redis為AP模式,可能存在獲取到多把鎖,比如在主從切換時,Zk為CP架構,不會存在獲取到多把鎖,因為在創建節點時會同步給半數節點。redis分布式鎖相對更快

設置鎖的超時時間,宕機後只會影響一定的時間

獲取鎖時判斷設置的值是否和當前線程的信息一樣

使用watchDog進行續租

極端情況下,節點1獲取到了鎖,發送修改了值的請求,請求已經發送出去,但是還沒有發送到資源伺服器,此時節點1斷開了ZK連接,節點2拿到了鎖,也發送了修改值的命令,資源伺服器先收到節點2的請求,之後又收到了節點1的請求就會出錯

③ 分布式鎖是什麼

什麼是分布式鎖?實現分布式鎖的三種方式

在很多場景中,我們為了保證數據的最終一致性,需要很多的技術方案來支持,比如分布式事務、分布式鎖等。那具體什麼是分布式鎖,分布式鎖應用在哪些業務場景、如何來實現分布式鎖呢?

一 為什麼要使用分布式鎖

我們在開發應用的時候,如果需要對某一個共享變數進行多線程同步訪問的時候,可以使用我們學到的鎖進行處理,並且可以完美的運行,毫無Bug!

注意這是單機應用,後來業務發展,需要做集群,一個應用需要部署到幾台機器上然後做負載均衡,大致如下圖:

六、基於ZooKeeper的實現方式

ZooKeeper是一個為分布式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。基於ZooKeeper實現分布式鎖的步驟如下:

(1)創建一個目錄mylock;
(2)線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
(3)獲取mylock目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
(4)線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
(5)線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。

這里推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分布式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。

優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。

缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。

七、總結


上面的三種實現方式,沒有在所有場合都是完美的,所以,應根據不同的應用場景選擇最適合的實現方式。

在分布式環境中,對資源進行上鎖有時候是很重要的,比如搶購某一資源,這時候使用分布式鎖就可以很好地控制資源。
當然,在具體使用中,還需要考慮很多因素,比如超時時間的選取,獲取鎖時間的選取對並發量都有很大的影響,上述實現的分布式鎖也只是一種簡單的實現,主要是一種思想。

④ Redisson實現分布式鎖原理

如圖所示啊,石杉大佬畫的redisson分布式鎖原理。
大概總結下,保證我們的key落到一個集群里,並且加鎖操作是基於lua腳本的原子性操作,對於鎖延遲由watch dog控制。

具體可以看 https://www.cnblogs.com/AnXinliang/p/10019389.html

如果你對某個redis master實例,寫入了myLock這種鎖key的value,此時會非同步復制給對應的master slave實例。但是這個過程中一旦發生redis master宕機,主備切換,redis slave變為了redis master。

接著就會導致,客戶端2來嘗試加鎖的時候,在新的redis master上完成了加鎖,而客戶端1也以為自己成功加了鎖。

此時就會 導致多個客戶端對一個分布式鎖完成了加鎖。

這時系統在業務語義上一定會出現問題,導致各種臟數據的產生。

所以這個就是redis cluster,或者是redis master-slave架構的主從非同步復制導致的redis分布式鎖的最大缺陷:在redis master實例宕機的時候,可能導致多個客戶端同時完成加鎖。

如果主動結構redis架構模式下,我們想保證完全一致,必須重寫加鎖的邏輯了, 保證必須mater和slave同時加鎖成功,我們整個加鎖才是成功的 。

上面的2是對於單個主從結構我們可以這樣干,如果假設我們有多個相對獨立的master,無slave呢?我們在其中一個master上加了🔐,然後它掛了豈不是我們的數據會在剩下的結點會重新加鎖成功?

redis引入了 紅鎖 的概念:用Redis中的多個master實例,來獲取鎖,只有 大多數 實例獲取到了鎖,才算是獲取成功

具體的紅鎖演算法分為以下五步:

以上步驟來自redis 分布式鎖的解釋,如下

」相同的key和隨機值(隨機值用於唯一關系客戶端和key)在N個節點上請求鎖「
這里的隨機數是什麼?

這對於 避免刪除由另一個客戶端創建的鎖 很重要。例如,客戶端可能會獲取鎖,執行某些操作時被阻塞的時間超過鎖的有效時間(密鑰將過期的時間),然後移除已被其他客戶端獲取的鎖。使用 just DEL 是不安全的,因為客戶端可能會刪除另一個客戶端的鎖。使用上面的腳本,每個鎖都用一個隨機字元串「簽名」,所以只有當它仍然是客戶端試圖移除它時設置的鎖才會被移除。

這個隨機字元串應該是什麼?我們假設它是 20 個位元組 /dev/urandom ,但您可以找到更便宜的方法使其對您的任務足夠獨特。例如,一個安全的選擇是用 RC4 播種 /dev/urandom ,並從中生成一個偽隨機流。一個更簡單的解決方案是使用具有微秒精度的 UNIX 時間戳,將 時間戳與客戶端 ID 連接起來。它並不安全,但對於大多數環境來說可能就足夠了。

假設一共有5個Redis節點:A, B, C, D, E。設想發生了如下的事件序列:

為了應對這一問題,提出了 延遲重啟 (delayed restarts)的概念。

就是,一個節點崩潰後,先不立即重啟它,而是等待一段時間再重啟,這段時間應該大於鎖的有效時間(lock validity time)。
這樣的話,這個節點在重啟前所參與的鎖都會過期,它在重啟後就不會對現有的鎖造成影響。

客戶端1在獲得鎖之後發生了很長時間的GC pause,在此期間,它獲得的鎖過期了,而客戶端2獲得了鎖。
當客戶端1從GC pause中恢復過來的時候,它不知道自己持有的鎖已經過期了,它依然向共享資源( 比如 一個存儲服務)發起了寫數據請求,
而這時鎖實際上被客戶端2持有,因此兩個客戶端的寫請求就有可能沖突(鎖的互斥作用失效了)。

如何解決這個問題呢?引入了 fencing token 的概念:

首先:RedLock根據隨機字元串來作為單次鎖服務的token,這就意味著對於資源而言,無法根據鎖token來區分client持有的鎖所獲取的先後順序。

fencing token可以理解成採用全局遞增的序列替代隨機字元串,即 有序token ,作為鎖token來使用
流程:

假設有5個Redis節點A, B, C, D, E。

這個問題用Redis實現分布式鎖暫時無解。而生產環境這種情況是存在的。
時鍾跳躍是可以避免的,取決於基礎設施和運維; 時鍾跳躍是可以避免的,取決於基礎設施和運維;

redis是保持的AP而非CP,如果要追求強一致性可以使用zookeeper分布式鎖,但是zookeeper也不是完全沒問題,在出現網路顏值,客戶端與服務端失聯情況的時候也依然可能會出現分布式的問題。

⑤ 分布式鎖的三種實現方式

分布式鎖三種實現方式:
1、基於資料庫實現分布式鎖;
2、基於緩存(Redis等)實現分布式鎖;
3、基於Zookeeper實現分布式鎖。從性能角度(從高到低)來看:「緩存方式>Zookeeper方式>=資料庫方式」。

⑥ 分布式鎖的三種實現方式

分布式鎖的三種實現方式分別是:基於資料庫實現分布式鎖、基於緩存(Redis等)實現分布式鎖、基於Zookeeper實現分布式鎖。

一、基於資料庫實現分布式鎖

1、悲觀鎖

利用select … where … for update 排他鎖。

注意:其他附加功能與實現一基本一致,這里需要注意的是「where name=lock 」,name欄位必須要走索引,否則會鎖表。有些情況下,比如表不大,mysql優化器會不走這個索引,導致鎖表問題。

2、樂觀鎖

所謂樂觀鎖與前邊最大區別在於基於CAS思想,是不具有互斥性,不會產生鎖等待而消耗資源,操作過程中認為不存在並發沖突,只有update version失敗後才能覺察到,搶購和秒殺就是用了這種實現以防止超賣,通過增加遞增的版本號欄位實現樂觀鎖。

二、基於緩存(Redis等)實現分布式鎖

1、使用命令介紹:

(1)SETNX

SETNX key val:當且僅當key不存在時,set一個key為val的字元串,返回1;若key存在,則什麼都不做,返回0。

(2)expire

expire key timeout:為key設置一個超時時間,單位為second,超過這個時間鎖會自動釋放,避免死鎖。

(3)delete

delete key:刪除key

在使用Redis實現分布式鎖的時候,主要就會使用到這三個命令。

2、實現思想:

(1)獲取鎖的時候,使用setnx加鎖,並使用expire命令為鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值為一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。

(2)獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。

(3)釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放。

三、基於Zookeeper實現分布式鎖

ZooKeeper是一個為分布式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。

基於ZooKeeper實現分布式鎖的步驟如下:

(1)創建一個目錄mylock。

(2)線程A想獲取鎖就在mylock目錄下創建臨時順序節點。

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

(4)線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點。

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

(6)資料庫鎖分布式鎖擴展閱讀;

一、資料庫分布式鎖實現的缺點:

1、db操作性能較差,並且有鎖表的風險。

2、非阻塞操作失敗後,需要輪詢,佔用cpu資源。

3、長時間不commit或者長時間輪詢,可能會佔用較多連接資源。

二、Redis(緩存)分布式鎖實現的缺點:

1、鎖刪除失敗,過期時間不好控制。

2、非阻塞,操作失敗後,需要輪詢,佔用cpu資源。

三、ZK分布式鎖實現的缺點:

性能不如redis實現,主要原因是寫操作(獲取鎖釋放鎖)都需要在Leader上執行,然後同步到follower。

⑦ 分布式鎖原理

分布式鎖是控制分布式系統之間同步訪問共享資源的一種方式。原理就是,當我們要實現分布式鎖,最簡單的方式可能就是直接創建一張鎖表,然後通過操作該表中的數據來實現了。

就是要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。

在分布式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,這個時候,便需要使用到分布式鎖。

當在分布式模型下,數據可能只有一份,此時需要利用鎖的技術控制某一時刻修改數據的進程數。與單機模式下的鎖不同,分布式鎖不僅需要保證進程可見,還需要考慮進程與鎖之間的網路問題。

分布式情況下之所以問題變得復雜,主要就是需要考慮到網路的延時和不可靠。分布式鎖還是可以將標記存在內存,只是該內存不是某個進程分配的內存而是公共內存如Redis、Memcache。至於利用資料庫、文件等做鎖與單機的實現是一樣的,只要保證標記能互斥就行。

⑧ 什麼是分布式鎖實現方式有哪些

什麼是分布式鎖?實現分布式鎖的三種方式


在很多場景中,我們為了保證數據的最終一致性,需要很多的技術方案來支持,比如分布式事務、分布式鎖等。那具體什麼是分布式鎖,分布式鎖應用在哪些業務場景、如何來實現分布式鎖呢?

一 為什麼要使用分布式鎖

我們在開發應用的時候,如果需要對某一個共享變數進行多線程同步訪問的時候,可以使用我們學到的鎖進行處理,並且可以完美的運行,毫無Bug!

注意這是單機應用,後來業務發展,需要做集群,一個應用需要部署到幾台機器上然後做負載均衡,大致如下圖:

上圖可以看到,變數A存在三個伺服器內存中(這個變數A主要體現是在一個類中的一個成員變數,是一個有狀態的對象),如果不加任何控制的話,變數A同時都會在分配一塊內存,三個請求發過來同時對這個變數操作,顯然結果是不對的!即使不是同時發過來,三個請求分別操作三個不同內存區域的數據,變數A之間不存在共享,也不具有可見性,處理的結果也是不對的!

如果我們業務中確實存在這個場景的話,我們就需要一種方法解決這個問題!

為了保證一個方法或屬性在高並發情況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的情況下,可以使用並發處理相關的功能進行互斥控制。但是,隨著業務發展的需要,原單體單機部署的系統被演化成分布式集群系統後,由於分布式系統多線程、多進程並且分布在不同機器上,這將使原單機部署情況下的並發控制鎖策略失效,單純的應用並不能提供分布式鎖的能力。為了解決這個問題就需要一種跨機器的互斥機制來控制共享資源的訪問,這就是分布式鎖要解決的問題!

二、分布式鎖應該具備哪些條件

在分析分布式鎖的三種實現方式之前,先了解一下分布式鎖應該具備哪些條件:

1、在分布式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行;
2、高可用的獲取鎖與釋放鎖;
3、高性能的獲取鎖與釋放鎖;
4、具備可重入特性;
5、具備鎖失效機制,防止死鎖;
6、具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。

三、分布式鎖的三種實現方式

目前幾乎很多大型網站及應用都是分布式部署的,分布式場景中的數據一致性問題一直是一個比較重要的話題。分布式的CAP理論告訴我們「任何一個分布式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition

tolerance),最多隻能同時滿足兩項。」所以,很多系統在設計之初就要對這三者做出取捨。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證「最終一致性」,只要這個最終時間是在用戶可以接受的范圍內即可。

在很多場景中,我們為了保證數據的最終一致性,需要很多的技術方案來支持,比如分布式事務、分布式鎖等。有的時候,我們需要保證一個方法在同一時間內只能被同一個線程執行。

⑨ 分布式鎖

與分布式鎖對應的是【單機鎖】,我們在寫多線程程序時,避免同時操作一個共享變數而產生數據問題,通常會使用一把鎖來實現【互斥】,其使用范圍是在【同一個進程中】。(同一個進程內存是共享的,以爭搶同一段內存,來判斷是否搶到鎖)。

如果是多個進程,如何互斥呢。就要引入【分布式鎖】來解決這個問題。想要實現分布式鎖,必須藉助一個外部系統,所有進程都去這個系統上去【申請加鎖】。

而這個外部系統,必須要實現【互斥】的能力,即兩個請求同時進來,只會給一個進程返回成功,另一個返回失敗(或等待)。

這個外部系統,可以是 MySQL,也可以是 Redis 或 Zookeeper。但為了追求更好的性能,我們通常會選擇使用 Redis 或 Zookeeper 來做。

依賴mysql的行鎖 select for update。

一個特例,唯一索引。唯一索引是找到了就直接停止遍歷,非唯一索引還會向後遍歷一行。移步第八個case。

現在的索引:

分析:

單條命中只會加行鎖,不加間隙鎖。所以RC/RR是一樣的。

事務1對id=10這條記錄加行鎖。所以 場景1會鎖等待,場景2不會鎖等待

分析:

RC隔離級別:

事務1未命中,不會加任何鎖。所以 場景1,場景2,場景3都不會鎖等待

RR隔離級別:

事務1未命中,會加間隙鎖。因為主鍵查詢,只會對主鍵加鎖。

在10和18加間隙鎖。

間隙鎖和查詢不沖突。 場景1不會鎖等待

間隙鎖和插入沖突。 場景2和場景3會鎖等待

分析

單條命中只會加行鎖,不加間隙鎖。所以RC/RR是一樣的。

事務1對二級索引和主鍵索引加行鎖。 事務1和事務2都會發生鎖等待

分析

RC隔離級別:

事務1未命中,不會加任何鎖。所以 場景1,場景2,場景3都不會鎖等待

RR隔離級別:

事務1對二級索引N0007到正無窮上間隙鎖,主鍵索引不上鎖。 場景1會鎖等待,場景2不會鎖等待

分析:

RC隔離級別:

只會加行鎖。 場景1場景2會鎖等待 場景3不會發生鎖等待

RR隔離級別:

會加行鎖和間隙鎖。 場景1場景2場景3都會鎖等待

ps: 如果是唯一索引,只會加行鎖。非唯一才會加間隙鎖。

RC隔離級別:

事務1未命中,不會加任何鎖。所以 場景1,場景2都不會鎖等待

RR隔離級別:

事務1未命中,會加間隙鎖。間隙鎖與查詢不沖突, 場景1不會發生鎖等待 場景2會發生鎖等待

分析

RC隔離級別:

事務1加了三個行鎖。 場景1會鎖等待。場景2,場景3不會發生鎖等待

RR隔離級別:

事務1加個三個行鎖和間隙鎖。 場景1,場景3會發生鎖等待 。間隙鎖與查詢不沖突, 場景2不會鎖等待。

分析:

RC隔離級別:

事務1加的都是行鎖。 場景1會發生鎖等待 場景2,場景3不會發生鎖等待

RR隔離級別:

事務1會對二級索引加行鎖和間隙鎖,對主鍵索引加行鎖。

場景1,場景3會發生鎖等待 。間隙鎖與查詢不沖突, 場景2不會鎖等待。

這么看,二級索引和唯一索引沒什麼區別。

那如果是 select * from book where name < 'Jim' for update; 呢

如果name是唯一索引。因為找到jim就不會向後遍歷了,所以jim和rose之間不會有間隙鎖。

分析:

RC隔離級別:

由於沒有走索引,所以只能全表掃描。在命中的主鍵索引上加行鎖。 場景1會鎖等待,場景2不會鎖等待

RR隔離級別:

不開啟innodb_locks_unsafe_for_binlog。 會發生鎖表

開啟innodb_locks_unsafe_for_binlog。和RC隔離級別一樣。

RC隔離級別:

未命中不加鎖。 場景1,場景2都不會鎖等待

RR隔離級別:

未命中, 鎖表

在RR隔離級別下,where條件沒有索引,都會鎖表。

加鎖命令:

釋放鎖命令:

這里存在問題,當釋放鎖之前異常退出了。這個鎖就永遠不會被釋放了。

怎麼解決呢?加一個超時時間。

還有問題,不是原子操作。

redis 2.6.12之後,redis天然支持了

來看一下還有什麼問題:

試想這樣一個場景

看到了么,這里存在兩個嚴重的問題:

釋放別人的鎖 :客戶端 1 操作共享資源完成後,卻又釋放了客戶端 2 的鎖

鎖過期 :客戶端 1 操作共享資源耗時太久,導致鎖被自動釋放,之後被客戶端 2 持有

解決辦法是:客戶端在加鎖時,設置一個只有自己知道的【唯一標識】進去。

例如,可以是自己的線程id,也可以是一個uuid

在釋放鎖時,可以這么寫:

問題來了,還不是原子的。redis沒有原生命令了。這里需要使用lua腳本

鎖的過期時間如果評估不好,這個鎖就會有「提前」過期的風險,一般的妥協方案是,盡量「冗餘」過期時間,降低鎖提前過期的概率。

其實可以有比較好的方案:

加鎖時,先設置一個過期時間,然後我們開啟一個「守護****線程****」,定時去檢測這個鎖的失效時間,如果鎖快要過期了,操作共享資源還未完成,那麼就自動對鎖進行「續期」,重新設置過期時間。

這個守護線程我們一般把他叫做【看門狗】線程。

我們在使用 Redis 時,一般會採用 主從集群 + 哨兵 的模式部署,這樣做的好處在於,當主庫異常宕機時,哨兵可以實現「故障自動切換」,把從庫提升為主庫,繼續提供服務,以此保證可用性。

試想這樣的場景:

為此,Redis 的作者提出一種解決方案,就是我們經常聽到的 Redlock(紅鎖)

現在我們來看,Redis 作者提出的 Redlock 方案,是如何解決主從切換後,鎖失效問題的。

Redlock 的方案基於 2 個前提:

也就是說,想用使用 Redlock,你至少要部署 5 個 Redis 實例,而且都是主庫,它們之間沒有任何關系,都是一個個孤立的實例。

Redlock 具體如何使用呢?

整體的流程是這樣的,一共分為 5 步:

有 4 個重點:

1) 為什麼要在多個實例上加鎖?

本質上是為了「容錯」,部分實例異常宕機,剩餘的實例加鎖成功,整個鎖服務依舊可用。

2) 為什麼大多數加鎖成功,才算成功?

多個 Redis 實例一起來用,其實就組成了一個「分布式系統」。

在分布式系統中,總會出現「異常節點」,所以,在談論分布式系統問題時,需要考慮異常節點達到多少個,也依舊不會影響整個系統的「正確性」。

這是一個分布式系統「容錯」問題,這個問題的結論是: 如果只存在「故障」節點,只要大多數節點正常,那麼整個系統依舊是可以提供正確服務的。

3) 為什麼步驟 3 加鎖成功後,還要計算加鎖的累計耗時?

因為操作的是多個節點,所以耗時肯定會比操作單個實例耗時更久,而且,因為是網路請求,網路情況是復雜的,有可能存在 延遲、丟包、超時 等情況發生,網路請求越多,異常發生的概率就越大。

所以,即使大多數節點加鎖成功,但如果加鎖的累計耗時已經「超過」了鎖的過期時間,那此時有些實例上的鎖可能已經失效了,這個鎖就沒有意義了。

4) 為什麼釋放鎖,要操作所有節點?

在某一個 Redis 節點加鎖時,可能因為「網路原因」導致加鎖失敗。

例如,客戶端在一個 Redis 實例上加鎖成功,但在讀取響應結果時,網路問題導致 讀取失敗 ,那這把鎖其實已經在 Redis 上加鎖成功了。

所以,釋放鎖時,不管之前有沒有加鎖成功,需要釋放「所有節點」的鎖,以保證清理節點上「殘留」的鎖。

好了,明白了 Redlock 的流程和相關問題,看似 Redlock 確實解決了 Redis 節點異常宕機鎖失效的問題,保證了鎖的「安全性」。

在martin的文章中,主要闡述了4個論點:

第一:效率

使用分布式鎖的互斥能力,是避免不必要地做同樣的工作兩次。如果鎖失效,並不會帶來「惡性」的後果,例如發了 2 次郵件等,無傷大雅。

第二:正確性

使用鎖用來防止並發進程相互干擾。如果鎖失敗,會造成多個進程同時操作同一條數據,產生的後果是數據嚴重錯誤、永久性不一致、數據丟失等惡性問題,後果嚴重。

他認為,如果你是為了前者——效率,那麼使用單機版redis就可以了,即使偶爾發生鎖失效(宕機、主從切換),都不會產生嚴重的後果。而使用redlock太重了,沒必要。

而如果是為了正確性,他認為redlock根本達不到安全性的要求,也依舊存在鎖失效的問題!

一個分布式系統,存在著你想不到的各種異常。這些異常場景主要包括三大塊,這也是分布式系統會遇到的三座大山: NPC

martin用一個進程暫停(GC)的例子,指出了redlock安全性的問題:

又或者,當多個Redis節點時鍾發生了問題時,也會導致redlock鎖失效。

在混亂的分布式系統中,你不能假設系統時鍾就是對的。

個人理解,相當於在業務層再做一層樂觀鎖。

一個好的分布式鎖,無論 NPC 怎麼發生,可以不在規定時間內給出結果,但並不會給出一個錯誤的結果。也就是只會影響到鎖的「性能」(或稱之為活性),而不會影響它的「正確性」。

1、Redlock 不倫不類 :它對於效率來講,Redlock 比較重,沒必要這么做,而對於正確性來說,Redlock 是不夠安全的。

2、時鍾假設不合理 :該演算法對系統時鍾做出了危險的假設(假設多個節點機器時鍾都是一致的),如果不滿足這些假設,鎖就會失效。

3、無法保證正確性 :Redlock 不能提供類似 fencing token 的方案,所以解決不了正確性的問題。為了正確性,請使用有「共識系統」的軟體,例如 Zookeeper。

好了,以上就是 Martin 反對使用 Redlock 的觀點,看起來有理有據。

下面我們來看 Redis 作者 Antirez 是如何反駁的。

首先,Redis 作者一眼就看穿了對方提出的最為核心的問題: 時鍾問題

Redis 作者表示,Redlock 並不需要完全一致的時鍾,只需要大體一致就可以了,允許有「誤差」。

例如要計時 5s,但實際可能記了 4.5s,之後又記了 5.5s,有一定誤差,但只要不超過「誤差范圍」鎖失效時間即可,這種對於時鍾的精度的要求並不是很高,而且這也符合現實環境。

對於對方提到的「時鍾修改」問題,Redis 作者反駁到:

Redis 作者繼續論述,如果對方認為,發生網路延遲、進程 GC 是在客戶端確認拿到了鎖,去操作共享資源的途中發生了問題,導致鎖失效,那這 不止是 Redlock 的問題,任何其它鎖服務例如 Zookeeper,都有類似的問題,這不在討論范疇內

這里我舉個例子解釋一下這個問題:

Redis 作者這里的結論就是:

所以,Redis 作者認為 Redlock 在保證時鍾正確的基礎上,是可以保證正確性的。

這個方案必須要求要操作的「共享資源伺服器」有拒絕「舊 token」的能力。

例如,要操作 MySQL,從鎖服務拿到一個遞增數字的 token,然後客戶端要帶著這個 token 去改 MySQL 的某一行,這就需要利用 MySQL 的「事物隔離性」來做。

但如果操作的不是 MySQL 呢?例如向磁碟上寫一個文件,或發起一個 HTTP 請求,那這個方案就無能為力了,這對要操作的資源伺服器,提出了更高的要求。

也就是說,大部分要操作的資源伺服器,都是沒有這種互斥能力的。

再者,既然資源伺服器都有了「互斥」能力,那還要分布式鎖干什麼?

利用 zookeeper 的同級節點的唯一性特性,在需要獲取排他鎖時,所有的客戶端試圖通過調用 create() 介面,在 /exclusive_lock 節點下創建臨時子節點 /exclusive_lock/lock ,最終只有一個客戶端能創建成功,那麼此客戶端就獲得了分布式鎖。同時,所有沒有獲取到鎖的客戶端可以在 /exclusive_lock 節點上注冊一個子節點變更的 watcher 監聽事件,以便重新爭取獲得鎖。

鎖釋放依賴心跳。集群中佔用鎖的客戶端失聯時,鎖能夠被有效釋放。一旦佔用Znode鎖的客戶端與ZooKeeper集群伺服器失去聯系,這個臨時Znode也將自動刪除

zookeeper的高可用依賴zab。簡單的說就是寫入時,半數follower ack,寫入成功。

zk是100%安全的么:

分析一個例子:

所以,得出一個結論: 一個分布式鎖,在極端情況下,不一定是安全的。

redlock運維成本也比較高。單機有高可用問題。所以還是主從+哨兵這樣的部署方式會好一些。

redis的缺點是:不是100%可靠。

mysql的缺點是:扛不住高流量請求。

可以二者結合,先用redis做分布式鎖,扛住大部分流量緩解mysql壓力。最後一定要用mysql做兜底保證100%的正確性。

⑩ Redis 分布式鎖詳細分析

鎖的作用,我想大家都理解,就是讓不同的線程或者進程可以安全地操作共享資源,而不會產生沖突。
比較熟悉的就是 Synchronized 和 ReentrantLock 等,這些可以保證同一個 jvm 程序中,不同線程安全操作共享資源。
但是在分布式系統中,這種方式就失效了;由於分布式系統多線程、多進程並且分布在不同機器上,這將使單機並發控制鎖策略失效,為了解決這個問題就需要一種跨 JVM 的互斥機制來控制共享資源的訪問。
比較常用的分布式鎖有三種實現方式:

本篇文章主要講解基於 Redis 分布式鎖的實現。

分布式鎖最主要的作用就是保證任意一個時刻,只有一個客戶端能訪問共享資源。

我們知道 redis 有 SET key value NX 命令,僅在不存在 key 的時候才能被執行成功,保證多個客戶端只有一個能執行成功,相當於獲取鎖。
釋放鎖的時候,只需要刪除 del key 這個 key 就行了。

上面的實現看似已經滿足要求了,但是忘了考慮在分布式環境下,有以下問題:

最大的問題就是因為客戶端或者網路問題,導致 redis 中的 key 沒有刪除,鎖無法釋放,因此其他客戶端無法獲取到鎖。

針對上面的情況,使用了下面命令:

使用 PX 的命令,給 key 添加一個自動過期時間(30秒),保證即使因為意外情況,沒有調用釋放鎖的方法,鎖也會自動釋放,其他客戶端仍然可以獲取到鎖。

注意給這個 key 設置的值 my_random_value 是一個隨機值,而且必須保證這個值在客戶端必須是唯一的。這個值的作用是為了更加安全地釋放鎖。

這是為了避免刪除其他客戶端成功獲取的鎖。考慮下面情況:

因此這里使用一個 my_random_value 隨機值,保證客戶端只會釋放自己獲取的鎖,即只刪除自己設置的 key 。

這種實現方式,存在下面問題:

上面章節介紹了,簡單實現存在的問題,下面來介紹一下 Redisson 實現又是怎麼解決的這些問題的。

主要關注 tryAcquireOnceAsync 方法,有三個參數:

方法主要流程:

這個方法的流程與 tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法基本相同。

這個方法與 tryAcquireOnceAsync 方法的區別,就是一個獲取鎖過期時間,一個是能否獲取鎖。即 獲取鎖過期時間 為 null 表示獲取到鎖,其他表示沒有獲取到鎖。

獲取鎖最終都會調用這個方法,通過 lua 腳本與 redis 進行交互,來實現分布式鎖。
首先分析,傳給 lua 腳本的參數:

lua 腳本的流程:

為了實現無限制持有鎖,那麼就需要定時刷新鎖的過期時間。

這個類最重要的是兩個成員屬性:

使用一個靜態並發集合 EXPIRATION_RENEWAL_MAP 來存儲所有鎖對應的 ExpirationEntry ,當有新的 ExpirationEntry 並存入到 EXPIRATION_RENEWAL_MAP 集合中時,需要調用 renewExpiration 方法,來刷新過期時間。

創建一個超時任務 Timeout task ,超時時間是 internalLockLeaseTime / 3 , 過了這個時間,即調用 renewExpirationAsync(threadId) 方法,來刷新鎖的過期時間。

判斷如果是當前線程持有的鎖,那麼就重新設置過期時間,並返回 1 即 true 。否則返回 0 即 false 。

通過調用 unlockInnerAsync(threadId) 來刪除 redis 中的 key 來釋放鎖。特別注意一點,當不是持有鎖的線程釋放鎖時引起的失敗,不需要調用 cancelExpirationRenewal 方法,取消定時,因為鎖還是被其他線程持有。

傳給這個 lua 腳本的值:

這個 lua 腳本的流程:

調用了 LockPubSub 的 subscribe 進行訂閱。

這個方法的作用就是向 redis 發起訂閱,但是對於同一個鎖的同一個客戶端(即 一個 jvm 系統) 只會發起一次訂閱,同一個客戶端的其他等待同一個鎖的線程會記錄在 RedissonLockEntry 中。

方法流程:

只有當 counter >= permits 的時候,回調 listener 才會運行,起到控制 listener 運行的效果。

釋放一個控制量,讓其中一個回調 listener 能夠運行。

主要屬性:

這個過程對應的 redis 中監控的命令日誌:

因為看門狗的默認時間是 30 秒,而定時刷新程序的時間是看門狗時間的 1/3 即 10 秒鍾,示常式序休眠了 15 秒,導致觸發了刷新鎖的過期時間操作。

注意 rLock.tryLock(10, TimeUnit.SECONDS); 時間要設置大一點,如果等待時間太短,小於獲取鎖 redis 命令的時間,那麼就直接返回獲取鎖失敗了。

分析源碼我們了解 Redisson 模式的分布式,解決了鎖過期時間和可重入的問題。但是針對 redis 本身可能存在的單點失敗問題,其實是沒有解決的。
基於這個問題, redis 作者提出了一種叫做 Redlock 演算法, 但是這種演算法本身也是有點問題的,想了解更多,請看 基於Redis的分布式鎖到底安全嗎?