Redis分佈式鎖詳細介紹

分佈式鎖

在單進程應用中,當一段代碼同一時間內隻能由一個線程執行時,

多線程下可能會出錯,例如兩個線程同時對一個數字做累加,兩個線程同時拿到瞭該數字,例如40,一個線程加瞭10,一個線程加瞭20,正確結果應該是70,

但由於兩個線程在自己的內存中一個算出的是50,一個算出的是60,此時二者都將自己的結果往該數字原本的地方寫(保存),

這時候,肯定會有一個線程的值會被覆蓋,因為讀取->計算->保存 並不是原子操作(原子操作是指不會被線程調度機制打斷的操作,這種操作一旦開始,就會一直運行結束,中間不會有任何線程切換),

也就是說最終的結果要麼是50,要麼是60,而不可能是70(出現並發或並行的情況下,這種情況大概率會發生),

image.png

單進程應用發生這種情況時,可以由程序提供的鎖語義直接上鎖(例如java中的sychornized)保證該段代碼隻會被一個線程執行,按照順序來進行,結果將是正確的。

在分佈式應用中,由於一臺機器上可能跑著相同的應用進程,或者在不同的機器上跑著,原本程序自帶的語義鎖已經無法起到作用,

因為相同的代碼可能是在不同的機器、進程中執行,所以此時需要一個能夠讓不同機器、進程中,相同的應用代碼執行到同一段代碼時,也能夠按照順序執行(或者同一時間內隻有一個線程能夠執行),

這就需要用到中間件來協調,可以實現分佈式鎖的中間件有很多,redis就是其中一個。

redis實現分佈式鎖的原理

redis中分佈式鎖的原理其實就是在redis當中設置一個值(當然要保證分佈式應用連的都是同一個redis,以這個redis作為中間點,否則當然是沒用的),這個值隻能由一個線程來存放,當其他線程(或者不同機器上的進程)也來存放時,發現這個值已經存在瞭,就說明此時已經有人在用這把鎖瞭,這時候要麼進行重試等待,要麼進行放棄。

設置一般使用 SETNX (set if not exists) 指令,如果該值沒有,則進行設置,有瞭則不設置,這就是拿鎖的關鍵瞭,當拿到鎖的人執行處理完畢後,再調用 DEL 執行進行鎖的釋放。

死鎖問題

使用 SETNX 和 DEL 實現瞭分佈式鎖,但是有一種情況,如果一個線程進行瞭 SETNX 拿到鎖成功後,突然這個線程因為某種原因崩潰瞭,導致沒有進行 DEL 釋放鎖,

那麼此時,將會導致其他所有的應用都再也沒辦法拿到這把鎖,也就是 死鎖 ,這個問題的解決方式是將鎖設置一個有效期,到瞭有效期之後該鎖將被自動釋放,

使用 EXPIRE 可以給鎖設置一個有效期,如下

SETNX LOCK-KEY-NAME true
EXPIRE LOCK-KEY-NAME 5

但是還有一個問題,因為 SETNX 和 EXPIRE 是分為兩個指令執行的,這中間依然有可能出現 SETNX 執行完畢後,由於認為或者機器、程序發生的故障 導致 EXPIRE 沒有執行成功,此時還是有可能會發生死鎖,

image.png

事務能不能解決這個問題?

NO,因為 EXPIRE 是依賴 SETNX 的執行結果執行的,隻有 SETNX 成功後,才能進行 EXPIRE,否則是不可以執行的,事務裡並沒有 if else 的分支邏輯,要麼全部執行,要麼一個都不執行。

在 redis2.8 的版本中,引入瞭set指令的拓展參數,可以讓 SETNX 和 EXPIRE 同時執行(原子),解決瞭超時問題,

SET LOCK-KEY-NAME true ex 5 nx

超時問題

上面雖然說到利用給鎖設置過期時間解決可能會發生的死鎖問題,但是萬一我的程序代碼執行時間超過瞭設置的過期時間,這時候鎖自動釋放瞭,但是我的代碼還沒執行完畢,其他人又進行執行瞭,導致結果出錯怎麼辦?

在一般的開發場景中,我們會盡量將鎖的時間設置的長一些,例如60s,一般應用程序在60s內都能執行完畢,但是怕就怕的是較真,如果60s內也執行不完怎麼辦?

此時可以使用一種續期的方案,就是當程序在執行過程中,不斷的判斷鎖是否快要過期,代碼是否執行完畢,如果快過期瞭沒有執行完畢,就將這把鎖進行續期,保證鎖不會被自動釋放,直到我們的代碼執行完畢為止,這種方案在java中由一個叫做 redisson 的框架實現瞭,可以直接引入使用。

鎖誤放問題

在鎖的使用過程中,很有可能出現其他應用沒有拿到鎖,但是也執行瞭 DEL 指令,將我們正在執行中的程序的鎖釋放瞭,導致其他地方拿到鎖,進入代碼段開始執行,

這裡的解決方式是,在SETNX的時候,可以將value設置成一個隨機生成並全局唯一的一串數字或字符,該線程一直持有字符,在釋放鎖的時候,將字符與鎖中的字符進行比對,如果匹配,則可以進行釋放鎖,如果不匹配,說明是其他人誤放,此時拒絕釋放,

但是判斷字符是否相同與釋放鎖並不是原子操作,redis也並不提供這麼一種命令,所以我們考慮使用lua腳本執行這幾步操作(redisson也實現瞭),

最重要的一點是,程序中使用釋放鎖的入口一定要統一,萬一有的應用程序不使用上面所述的方法釋放,直接使用 DEL ,那麼上面說的方案就沒用瞭(筆者為瞭測試,直接用DEL釋放過)。

可重入性

可重入性是指線程在持有鎖的情況下,再次請求加鎖,如果一個鎖支持同一個線程的多次加鎖,那麼這個鎖就是可重入的,Java中的 ReentrantLock 就是可重入鎖,大致的原理就是每次獲取到鎖後對一個數字進行 +1,每次釋放的時候進行 -1,當數字為0時,分佈式鎖被釋放,

redis鎖如果要支持可重入性,也需要用上面的方式進行支持,不過該邏輯加重瞭復雜性,一般推薦將需要鎖的代碼段進行邏輯調整,避免重復獲取分佈式鎖來處理。(當然redisson也支持瞭可重入鎖)

Redlock

上面的方式看起來沒有太多的問題瞭,但是由於redis本身可能也會發生問題,例如在Sentinel集群中,主節點掛掉,從節點變成主節點,但是客戶端這時候是不知道的,

如果客戶端在剛剛掛掉的主節點上SETNX成功瞭,但是這把鎖還沒有同步到從節點中,從節點這時候變成瞭主節點,這時候新主節點中沒有這把鎖的信息,

此時另一個客戶端來請求這把鎖,直接 SETNX 成功,又導致瞭兩個客戶端同時在執行相同的代碼,又出現瞭不安全性,

image.png

為此業界提供瞭叫做 Redlock 的解決方案,原理大致是,提供多臺redis實例,這些實例之間相互獨立,沒有主從關系(沒有任何關系),同其他分佈式算法一樣,使用瞭大多數機制,

加鎖時,它會向過半節點發出 set(key, value, nx = True, ex = xxx)指令,隻要過半節點設置成功,這把鎖就算拿到瞭,釋放鎖時向所有節點發出 DEL 指令,

Redlock算法(Redisson已支持)需要考慮出錯重試,時鐘漂移等等細節問題,同時Redlock需要向多個節點進行讀寫,性能將要比單例redis下降,

如果業務場景對錯誤的發生容忍度很低,又可以接受性能稍微有點下降,可以考慮采用Redlock算法。

到此這篇關於Redis分佈式鎖詳細介紹的文章就介紹到這瞭,更多相關Redis分佈式鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: