什麼是 “分佈式鎖” ?
說說分佈式鎖吧?
對於一個單機的系統,我們可以通過 synchronized 或者 ReentrantLock 等這些常規的加鎖方式來實現,然而對於一個分佈式集羣的系統而言,單純的本地鎖已經無法解決問題,所以就需要用到分佈式鎖了,通常我們都會引入三方組件或者服務來解決這個問題,比如數據庫、Redis、Zookeeper 等。
通常來說,分佈式鎖要保證互斥性、不死鎖、可重入等特點。
互斥性指的是對於同一個資源,任意時刻,都只有一個客戶端能持有鎖。
不死鎖指的是必須要有鎖超時這種機制,保證在出現問題的時候釋放鎖,不會出現死鎖的問題。
可重入指的是對於同一個線程,可以多次重複加鎖。
那你分別說說使用數據庫、Redis 和 Zookeeper 的實現原理?
數據庫的話可以使用樂觀鎖或者悲觀鎖的實現方式。
但是這種實現方式把加鎖和設置過期時間的步驟分成兩步,他們並不是原子操作,如果加鎖成功之後程序崩潰、服務宕機等異常情況,導致沒有設置過期時間,那麼就會導致死鎖的問題,其他線程永遠都無法獲取這個鎖。
之後的版本中,Redis 提供了原生的set
命令,相當於兩命令合二爲一,不存在原子性的問題,當然也可以通過 lua 腳本來解決。
set
命令如下格式:
key 爲分佈式鎖的 key
value 爲分佈式鎖的值,一般爲不同的客戶端設置不同的值
NX 代表如果要設置的 key 已存在,則取消設置
EX 代表過期時間爲秒,PX 則爲毫秒,比如上面示例中爲 10 秒過期
Zookeeper 是通過創建臨時順序節點的方式來實現。
-
當需要對資源進行加鎖時,實際上就是在父節點之下創建一個臨時順序節點。
-
客戶端 A 來對資源加鎖,首先判斷當前創建的節點是否爲最小節點,如果是,那麼加鎖成功,後續加鎖線程阻塞等待
-
此時,客戶端 B 也來嘗試加鎖,由於客戶端 A 已經加鎖成功,所以客戶端 B 發現自己的節點並不是最小節點,就會去取到上一個節點,並且對上一節點註冊監聽
-
當客戶端 A 操作完成,釋放鎖的操作就是刪除這個節點,這樣就可以觸發監聽事件,客戶端 B 就會得到通知,同樣,客戶端 B 判斷自己是否爲最小節點,如果是,那麼則加鎖成功
你說改爲 set 命令之後就解決了問題?那麼還會不會有其他的問題呢?
雖然set
解決了原子性的問題,但是還是會存在兩個問題。
鎖超時問題
比如客戶端 A 加鎖同時設置超時時間是 3 秒,結果 3s 之後程序邏輯還沒有執行完成,鎖已經釋放。客戶端 B 此時也來嘗試加鎖,那麼客戶端 B 也會加鎖成功。
這樣的話,就導致了併發的問題,如果代碼冪等性沒有處理好,就會導致問題產生。
鎖誤刪除
還是類似的問題,客戶端 A 加鎖同時設置超時時間 3 秒,結果 3s 之後程序邏輯還沒有執行完成,鎖已經釋放。客戶端 B 此時也來嘗試加鎖,這時客戶端 A 代碼執行完成,執行釋放鎖,結果釋放了客戶端 B 的鎖。
那上面兩個問題你有什麼好的解決方案嗎?
鎖超時
這個有兩個解決方案。
-
針對鎖超時的問題,我們可以根據平時業務執行時間做大致的評估,然後根據評估的時間設置一個較爲合理的超時時間,這樣能一大部分程度上避免問題。
-
自動續租,通過其他的線程爲將要過期的鎖延長持有時間
鎖誤刪除
每個客戶端的鎖只能自己解鎖,一般我們可以在使用set
命令的時候生成隨機的 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),這樣就是加鎖成功,反之如果最後時間 < 0,那麼加鎖失敗
-
如果加鎖失敗,那麼要釋放所有節點上的鎖
那麼 RedLock 有什麼問題嗎?
其實 RedLock 存在不少問題,所以現在其實一般不推薦使用這種方式,而是推薦使用 Redission 的方案,他的問題主要如下幾點。
性能、資源
因爲需要對多個節點分別加鎖和解鎖,而一般分佈式鎖的應用場景都是在高併發的情況下,所以耗時較長,對性能有一定的影響。此外因爲需要多個節點,使用的資源也比較多,簡單來說就是費錢。
節點崩潰重啓
比如有 1~5 號五個節點,並且沒有開啓持久化,客戶端 A 在 1,2,3 號節點加鎖成功,此時 3 號節點崩潰宕機後發生重啓,就丟失了加鎖信息,客戶端 B 在 3,4,5 號節點加鎖成功。
那麼,兩個客戶端 A\B 同時獲取到了同一個鎖,問題產生了,怎麼解決?
-
Redis 作者建議的方式就是延時重啓,比如 3 號節點宕機之後不要立刻重啓,而是等待一段時間後再重啓,這個時間必須大於鎖的有效時間,也就是鎖失效後再重啓,這種人爲干預的措施真正實施起來就比較困難了
-
第二個方案那麼就是開啓持久化,但是這樣對性能又造成了影響。比如如果開啓 AOF 默認每秒一次刷盤,那麼最多丟失一秒的數據,如果想完全不丟失的話就對性能造成較大的影響。
GC、網絡延遲
對於 RedLock,Martin Kleppmann 提出了很多質疑,我就只舉這樣一個 GC 或者網絡導致的例子。(這個問題比較多,我就不一一舉例了,心裏有一個概念就行了,文章地址:https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
)
從圖中我們可以看出,client1 線獲取到鎖,然後發生 GC 停頓,超過了鎖的有效時間導致鎖被釋放,然後鎖被 client2 拿到,然後兩個客戶端同時拿到鎖在寫數據,問題產生。
圖片來自 Martin Kleppmann
時鐘跳躍
同樣的例子,假設發生網絡分區,4、5 號節點變爲一個獨立的子網,3 號節點發生始終跳躍(不管人爲操作還是同步導致)導致鎖過期,這時候另外的客戶端就可以從 3、4、5 號節點加鎖成功,問題又發生了。
那你說說有什麼好的解決方案嗎?
上面也提到了,其實比較好的方式是使用Redission
,它是一個開源的 Java 版本的 Redis 客戶端,無論單機、哨兵、集羣環境都能支持,另外還很好地解決了鎖超時、公平非公平鎖、可重入等問題,也實現了RedLock
,同時也是官方推薦的客戶端版本。
那麼 Redission 實現原理呢?
加鎖、可重入
首先,加鎖和解鎖都是通過 lua 腳本去實現的,這樣做的好處是爲了兼容老版本的 redis 同時保證原子性。
KEYS[1]
爲鎖的 key,ARGV[2]
爲鎖的 value,格式爲 uuid + 線程 ID,ARGV[1]
爲過期時間。
主要的加鎖邏輯也比較容易看懂,如果key
不存在,通過 hash 的方式保存,同時設置過期時間,反之如果存在就是 + 1。
對應的就是hincrby', KEYS[1], ARGV[2], 1
這段命令,對 hash 結構的鎖重入次數 + 1。
解鎖
-
如果 key 都不存在了,那麼就直接返回
-
如果 key、field 不匹配,那麼說明不是自己的鎖,不能釋放,返回空
-
釋放鎖,重入次數 - 1,如果還大於 0 那麼久刷新過期時間,反之那麼久刪除鎖
watchdog
也叫做看門狗,也就是解決了鎖超時導致的問題,實際上就是一個後臺線程,默認每隔 10 秒自動延長鎖的過期時間。
默認的時間就是internalLockLeaseTime / 3
,internalLockLeaseTime
默認爲 30 秒。
最後,實際生產中對於不同的場景該如何選擇?
首先,如果對於併發不高並且比較簡單的場景,通過數據庫樂觀鎖或者唯一主鍵的形式就能解決大部分的問題。
然後,對於 Redis 實現的分佈式鎖來說性能高,自己去實現的話比較麻煩,要解決鎖續租、lua 腳本、可重入等一系列複雜的問題。
對於單機模式而言,存在單點問題。
對於主從架構或者哨兵模式,故障轉移會發生鎖丟失的問題,因此產生了紅鎖,但是紅鎖的問題也比較多,並不推薦使用,推薦的使用方式是用 Redission。
但是,不管選擇哪種方式,本身對於 Redis 來說不是強一致性的,某些極端場景下還是可能會存在問題。
對於 Zookeeper 的實現方式而言,本身就是保證數據一致性的,可靠性更高,所以不存在 Redis 的各種故障轉移帶來的問題,自己實現也比較簡單,但是性能相比 Redis 稍差。
不過,實際中我們當然是有啥用啥,老闆說用什麼就用什麼,我纔不管那麼多。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/hDtc9TTH2LeUJzje7PPydg