什麼是 “分佈式鎖” ?

說說分佈式鎖吧?

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

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

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

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

可重入指的是對於同一個線程,可以多次重複加鎖。

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

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

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

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

set命令如下格式:

key 爲分佈式鎖的 key

value 爲分佈式鎖的值,一般爲不同的客戶端設置不同的值

NX 代表如果要設置的 key 已存在,則取消設置

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

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

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

  2. 客戶端 A 來對資源加鎖,首先判斷當前創建的節點是否爲最小節點,如果是,那麼加鎖成功,後續加鎖線程阻塞等待

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

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

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

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

鎖超時問題

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

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

鎖誤刪除

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

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

鎖超時

這個有兩個解決方案。

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

  2. 自動續租,通過其他的線程爲將要過期的鎖延長持有時間

鎖誤刪除

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

瞭解 RedLock 算法嗎?

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

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

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

主要步驟如下:

  1. 獲取當前 Unix 時間

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

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

那麼 RedLock 有什麼問題嗎?

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

性能、資源

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

節點崩潰重啓

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

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

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

  2. 第二個方案那麼就是開啓持久化,但是這樣對性能又造成了影響。比如如果開啓 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。

解鎖

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

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

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

watchdog

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

默認的時間就是internalLockLeaseTime / 3internalLockLeaseTime默認爲 30 秒。

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

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

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

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

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

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

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

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/hDtc9TTH2LeUJzje7PPydg