選單

《我想進大廠》之分散式鎖奪命連環9問

說說分散式鎖吧?

對於一個單機的系統,我們可以透過synchronized或者ReentrantLock等這些常規的加鎖方式來實現,然而對於一個分散式叢集的系統而言,單純的本地鎖已經無法解決問題,所以就需要用到分散式鎖了,通常我們都會引入三方元件或者服務來解決這個問題,比如資料庫、Redis、Zookeeper等。

通常來說,分散式鎖要保證互斥性、不死鎖、可重入等特點。

互斥性指的是對於同一個資源,任意時刻,都只有一個客戶端能持有鎖。

不死鎖指的是必須要有鎖超時這種機制,保證在出現問題的時候釋放鎖,不會出現死鎖的問題。

可重入指的是對於同一個執行緒,可以多次重複加鎖。

那你分別說說使用資料庫、Redis和Zookeeper的實現原理?

資料庫的話可以使用樂觀鎖或者悲觀鎖的實現方式。

樂觀鎖通常就是資料庫中我們會有一個版本號,更新資料的時候透過版本號來更新,這樣的話效率會比較高,悲觀鎖則是透過的方式,但是會帶來很多問題,因為他是一個行級鎖,高併發的情況下可能會導致死鎖、客戶端連線超時等問題,一般不推薦使用這種方式。

Redis是透過命令來實現,在版本之前,實現方式可能是這樣:

《我想進大廠》之分散式鎖奪命連環9問

命令代表當不存在時返回成功,否則返回失敗。

但是這種實現方式把加鎖和設定過期時間的步驟分成兩步,他們並不是原子操作,如果加鎖成功之後程式崩潰、服務宕機等異常情況,導致沒有設定過期時間,那麼就會導致死鎖的問題,其他執行緒永遠都無法獲取這個鎖。

之後的版本中,Redis提供了原生的命令,相當於兩命令合二為一,不存在原子性的問題,當然也可以透過lua指令碼來解決。

命令如下格式:

《我想進大廠》之分散式鎖奪命連環9問

key 為分散式鎖的key

value 為分散式鎖的值,一般為不同的客戶端設定不同的值

NX 代表如果要設定的key存在返回成功,否則返回失敗

EX 代表過期時間為秒,PX則為毫秒,比如上面示例中為10秒過期

Zookeeper是透過建立臨時順序節點的方式來實現。

《我想進大廠》之分散式鎖奪命連環9問

當需要對資源進行加鎖時,實際上就是在父節點之下建立一個臨時順序節點。

客戶端A來對資源加鎖,首先判斷當前建立的節點是否為最小節點,如果是,那麼加鎖成功,後續加鎖執行緒阻塞等待

此時,客戶端B也來嘗試加鎖,由於客戶端A已經加鎖成功,所以客戶端B發現自己的節點並不是最小節點,就會去取到上一個節點,並且對上一節點註冊監聽

當客戶端A操作完成,釋放鎖的操作就是刪除這個節點,這樣就可以觸發監聽事件,客戶端B就會得到通知,同樣,客戶端B判斷自己是否為最小節點,如果是,那麼則加鎖成功

你說改為set命令之後就解決了問題?那麼還會不會有其他的問題呢?

雖然解決了原子性的問題,但是還是會存在兩個問題。

鎖超時問題

比如客戶端A加鎖同時設定超時時間是3秒,結果3s之後程式邏輯還沒有執行完成,鎖已經釋放。客戶端B此時也來嘗試加鎖,那麼客戶端B也會加鎖成功。

這樣的話,就導致了併發的問題,如果程式碼冪等性沒有處理好,就會導致問題產生。

《我想進大廠》之分散式鎖奪命連環9問

鎖誤刪除

還是類似的問題,客戶端A加鎖同時設定超時時間3秒,結果3s之後程式邏輯還沒有執行完成,鎖已經釋放。客戶端B此時也來嘗試加鎖,這時客戶端A程式碼執行完成,執行釋放鎖,結果釋放了客戶端B的鎖。

《我想進大廠》之分散式鎖奪命連環9問

那上面兩個問題你有什麼好的解決方案嗎?

鎖超時

這個有兩個解決方案。

針對鎖超時的問題,我們可以根據平時業務執行時間做大致的評估,然後根據評估的時間設定一個較為合理的超時時間,這樣能一大部分程度上避免問題。

自動續租,透過其他的執行緒為將要過期的鎖延長持有時間

鎖誤刪除

每個客戶端的鎖只能自己解鎖,一般我們可以在使用命令的時候生成隨機的value,解鎖使用lua指令碼判斷當前鎖是否自己持有的,是自己的鎖才能釋放。

瞭解RedLock演算法嗎?

因為在Redis的主從架構下,主從同步是非同步的,如果在Master節點加鎖成功後,指令還沒有同步到Slave節點,此時Master掛掉,Slave被提升為Master,新的Master上並沒有鎖的資料,其他的客戶端仍然可以加鎖成功。

對於這種問題,Redis作者提出了RedLock紅鎖的概念。

RedLock的理念下需要至少2個Master節點,多個Master節點之間完全互相獨立,彼此之間不存在主從同步和資料複製。

主要步驟如下:

獲取當前Unix時間

按照順序依次嘗試從多個節點鎖,如果獲取鎖的時間小於超時時間,並且超過半數的節點獲取成功,那麼加鎖成功。這樣做的目的就是為了避免某些節點已經宕機的情況下,客戶端還在一直等待響應結果。舉個例子,假設現在有5個節點,過期時間=100ms,第一個節點獲取鎖花費10ms,第二個節點花費20ms,第三個節點花費30ms,那麼最後鎖的過期時間就是100-(10+20+30),這樣就是加鎖成功,反之如果最後時間

如果加鎖失敗,那麼要釋放所有節點上的鎖

那麼RedLock有什麼問題嗎?

其實RedLock存在不少問題,所以現在其實一般不推薦使用這種方式,而是推薦使用Redission的方案,他的問題主要如下幾點。

效能、資源

因為需要對多個節點分別加鎖和解鎖,而一般分散式鎖的應用場景都是在高併發的情況下,所以耗時較長,對效能有一定的影響。此外因為需要多個節點,使用的資源也比較多,簡單來說就是費錢。

節點崩潰重啟

比如有1~5號五個節點,並且沒有開啟持久化,客戶端A在1,2,3號節點加鎖成功,此時3號節點崩潰宕機後發生重啟,就丟失了加鎖資訊,客戶端B在3,4,5號節點加鎖成功。

那麼,兩個客戶端A\B同時獲取到了同一個鎖,問題產生了,怎麼解決?

Redis作者建議的方式就是延時重啟,比如3號節點宕機之後不要立刻重啟,而是等待一段時間後再重啟,這個時間必須大於鎖的有效時間,也就是鎖失效後再重啟,這種人為干預的措施真正實施起來就比較困難了

第二個方案那麼就是開啟持久化,但是這樣對效能又造成了影響。比如如果開啟AOF預設每秒一次刷盤,那麼最多丟失一秒的資料,如果想完全不丟失的話就對效能造成較大的影響。

GC、網路延遲

對於RedLock,Martin Kleppmann提出了很多質疑,我就只舉這樣一個GC或者網路導致的例子。(這個問題比較多,我就不一一舉例了,心裡有一個概念就行了,文章地址:)

從圖中我們可以看出,client1線獲取到鎖,然後發生GC停頓,超過了鎖的有效時間導致鎖被釋放,然後鎖被client2拿到,然後兩個客戶端同時拿到鎖在寫資料,問題產生。

《我想進大廠》之分散式鎖奪命連環9問

圖片來自Martin Kleppmann

時鐘跳躍

同樣的例子,假設發生網路分割槽,4、5號節點變為一個獨立的子網,3號節點發生始終跳躍(不管人為操作還是同步導致)導致鎖過期,這時候另外的客戶端就可以從3、4、5號節點加鎖成功,問題又發生了。

那你說說有什麼好的解決方案嗎?

上面也提到了,其實比較好的方式是使用,它是一個開源的Java版本的Redis客戶端,無論單機、哨兵、叢集環境都能支援,另外還很好地解決了鎖超時、公平非公平鎖、可重入等問題,也實現了,同時也是官方推薦的客戶端版本。

那麼Redission實現原理呢?

加鎖、可重入

首先,加鎖和解鎖都是透過lua指令碼去實現的,這樣做的好處是為了相容老版本的redis同時保證原子性。

為鎖的key,為鎖的value,格式為uuid+執行緒ID,為過期時間。

主要的加鎖邏輯也比較容易看懂,如果不存在,透過hash的方式儲存,同時設定過期時間,反之如果存在就是+1。

對應的就是這段命令,對hash結構的鎖重入次數+1。

《我想進大廠》之分散式鎖奪命連環9問

解鎖

如果key都不存在了,那麼就直接返回

如果key、field不匹配,那麼說明不是自己的鎖,不能釋放,返回空

釋放鎖,重入次數-1,如果還大於0那麼久重新整理過期時間,反之那麼久刪除鎖

《我想進大廠》之分散式鎖奪命連環9問

watchdog

也叫做看門狗,也就是解決了鎖超時導致的問題,實際上就是一個後臺執行緒,預設每隔10秒自動延長鎖的過期時間。

預設的時間就是,預設為30秒。

《我想進大廠》之分散式鎖奪命連環9問

最後,實際生產中對於不同的場景該如何選擇?

首先,如果對於併發不高並且比較簡單的場景,透過資料庫樂觀鎖或者唯一主鍵的形式就能解決大部分的問題。

然後,對於Redis實現的分散式鎖來說效能高,自己去實現的話比較麻煩,要解決鎖續租、lua指令碼、可重入等一系列複雜的問題。

對於單機模式而言,存在單點問題。

對於主從架構或者哨兵模式,故障轉移會發生鎖丟失的問題,因此產生了紅鎖,但是紅鎖的問題也比較多,並不推薦使用,推薦的使用方式是用Redission。

但是,不管選擇哪種方式,本身對於Redis來說不是強一致性的,某些極端場景下還是可能會存在問題。

對於Zookeeper的實現方式而言,本身就是保證資料一致性的,可靠性更高,所以不存在Redis的各種故障轉移帶來的問題,自己實現也比較簡單,但是效能相比Redis稍差。

不過,實際中我們當然是有啥用啥,老闆說用什麼就用什麼,我才不管那麼多。