說說分散式鎖吧?
對於一個單機的系統,我們可以透過synchronized或者ReentrantLock等這些常規的加鎖方式來實現,然而對於一個分散式叢集的系統而言,單純的本地鎖已經無法解決問題,所以就需要用到分散式鎖了,通常我們都會引入三方元件或者服務來解決這個問題,比如資料庫、Redis、Zookeeper等。
通常來說,分散式鎖要保證互斥性、不死鎖、可重入等特點。
互斥性指的是對於同一個資源,任意時刻,都只有一個客戶端能持有鎖。
不死鎖指的是必須要有鎖超時這種機制,保證在出現問題的時候釋放鎖,不會出現死鎖的問題。
可重入指的是對於同一個執行緒,可以多次重複加鎖。
那你分別說說使用資料庫、Redis和Zookeeper的實現原理?
資料庫的話可以使用樂觀鎖或者悲觀鎖的實現方式。
樂觀鎖通常就是資料庫中我們會有一個版本號,更新資料的時候透過版本號來更新,這樣的話效率會比較高,悲觀鎖則是透過的方式,但是會帶來很多問題,因為他是一個行級鎖,高併發的情況下可能會導致死鎖、客戶端連線超時等問題,一般不推薦使用這種方式。
Redis是透過命令來實現,在版本之前,實現方式可能是這樣:
命令代表當不存在時返回成功,否則返回失敗。
但是這種實現方式把加鎖和設定過期時間的步驟分成兩步,他們並不是原子操作,如果加鎖成功之後程式崩潰、服務宕機等異常情況,導致沒有設定過期時間,那麼就會導致死鎖的問題,其他執行緒永遠都無法獲取這個鎖。
之後的版本中,Redis提供了原生的命令,相當於兩命令合二為一,不存在原子性的問題,當然也可以透過lua指令碼來解決。
命令如下格式:
key 為分散式鎖的key
value 為分散式鎖的值,一般為不同的客戶端設定不同的值
NX 代表如果要設定的key存在返回成功,否則返回失敗
EX 代表過期時間為秒,PX則為毫秒,比如上面示例中為10秒過期
Zookeeper是透過建立臨時順序節點的方式來實現。
當需要對資源進行加鎖時,實際上就是在父節點之下建立一個臨時順序節點。
客戶端A來對資源加鎖,首先判斷當前建立的節點是否為最小節點,如果是,那麼加鎖成功,後續加鎖執行緒阻塞等待
此時,客戶端B也來嘗試加鎖,由於客戶端A已經加鎖成功,所以客戶端B發現自己的節點並不是最小節點,就會去取到上一個節點,並且對上一節點註冊監聽
當客戶端A操作完成,釋放鎖的操作就是刪除這個節點,這樣就可以觸發監聽事件,客戶端B就會得到通知,同樣,客戶端B判斷自己是否為最小節點,如果是,那麼則加鎖成功
你說改為set命令之後就解決了問題?那麼還會不會有其他的問題呢?
雖然解決了原子性的問題,但是還是會存在兩個問題。
鎖超時問題
比如客戶端A加鎖同時設定超時時間是3秒,結果3s之後程式邏輯還沒有執行完成,鎖已經釋放。客戶端B此時也來嘗試加鎖,那麼客戶端B也會加鎖成功。
這樣的話,就導致了併發的問題,如果程式碼冪等性沒有處理好,就會導致問題產生。
鎖誤刪除
還是類似的問題,客戶端A加鎖同時設定超時時間3秒,結果3s之後程式邏輯還沒有執行完成,鎖已經釋放。客戶端B此時也來嘗試加鎖,這時客戶端A程式碼執行完成,執行釋放鎖,結果釋放了客戶端B的鎖。
那上面兩個問題你有什麼好的解決方案嗎?
鎖超時
這個有兩個解決方案。
針對鎖超時的問題,我們可以根據平時業務執行時間做大致的評估,然後根據評估的時間設定一個較為合理的超時時間,這樣能一大部分程度上避免問題。
自動續租,透過其他的執行緒為將要過期的鎖延長持有時間
鎖誤刪除
每個客戶端的鎖只能自己解鎖,一般我們可以在使用命令的時候生成隨機的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拿到,然後兩個客戶端同時拿到鎖在寫資料,問題產生。
圖片來自Martin Kleppmann
時鐘跳躍
同樣的例子,假設發生網路分割槽,4、5號節點變為一個獨立的子網,3號節點發生始終跳躍(不管人為操作還是同步導致)導致鎖過期,這時候另外的客戶端就可以從3、4、5號節點加鎖成功,問題又發生了。
那你說說有什麼好的解決方案嗎?
上面也提到了,其實比較好的方式是使用,它是一個開源的Java版本的Redis客戶端,無論單機、哨兵、叢集環境都能支援,另外還很好地解決了鎖超時、公平非公平鎖、可重入等問題,也實現了,同時也是官方推薦的客戶端版本。
那麼Redission實現原理呢?
加鎖、可重入
首先,加鎖和解鎖都是透過lua指令碼去實現的,這樣做的好處是為了相容老版本的redis同時保證原子性。
為鎖的key,為鎖的value,格式為uuid+執行緒ID,為過期時間。
主要的加鎖邏輯也比較容易看懂,如果不存在,透過hash的方式儲存,同時設定過期時間,反之如果存在就是+1。
對應的就是這段命令,對hash結構的鎖重入次數+1。
解鎖
如果key都不存在了,那麼就直接返回
如果key、field不匹配,那麼說明不是自己的鎖,不能釋放,返回空
釋放鎖,重入次數-1,如果還大於0那麼久重新整理過期時間,反之那麼久刪除鎖
watchdog
也叫做看門狗,也就是解決了鎖超時導致的問題,實際上就是一個後臺執行緒,預設每隔10秒自動延長鎖的過期時間。
預設的時間就是,預設為30秒。
最後,實際生產中對於不同的場景該如何選擇?
首先,如果對於併發不高並且比較簡單的場景,透過資料庫樂觀鎖或者唯一主鍵的形式就能解決大部分的問題。
然後,對於Redis實現的分散式鎖來說效能高,自己去實現的話比較麻煩,要解決鎖續租、lua指令碼、可重入等一系列複雜的問題。
對於單機模式而言,存在單點問題。
對於主從架構或者哨兵模式,故障轉移會發生鎖丟失的問題,因此產生了紅鎖,但是紅鎖的問題也比較多,並不推薦使用,推薦的使用方式是用Redission。
但是,不管選擇哪種方式,本身對於Redis來說不是強一致性的,某些極端場景下還是可能會存在問題。
對於Zookeeper的實現方式而言,本身就是保證資料一致性的,可靠性更高,所以不存在Redis的各種故障轉移帶來的問題,自己實現也比較簡單,但是效能相比Redis稍差。
不過,實際中我們當然是有啥用啥,老闆說用什麼就用什麼,我才不管那麼多。