天天看點

Redis 鎖

Redis 鎖

  • ​​1. 分布式鎖背景​​
  • ​​2. 鎖的基本特性​​
  • ​​3. 基于Redis實作鎖的基本原理​​
  • ​​4. redis實作鎖​​
  • ​​5. redis分布式鎖​​
  • ​​6. 擷取鎖失敗​​
  • ​​7. 最終​​
  • ​​8. 使用​​

1. 分布式鎖背景

在單體機器的jvm中,多個線程想要通路共享資源,那麼,需要在jvm中建立一個獨占鎖,哪個線程擷取到了鎖,那麼這個線程可以通路資源。其他線程隻能等待擷取到鎖的線程釋放鎖。

在多體機器的叢集環境中,仍然是多個線程想要通路共享資源。但是因為這些線程并不在一個jvm中,是以建立獨占鎖就不能實作不同機器的jvm内的線程等待。是以就需要引入第三方鎖,叢集内的所有jvm都可以通路第三方鎖,哪一個機器的得到了鎖,那麼這個機器的線程就可以通路資源,其他機器的線程隻能等待釋放鎖。

第三方鎖

機器A

機器B

機器C擷取到了鎖

2. 鎖的基本特性

  • 安全:獨占鎖。在任意一個時刻,隻有一個用戶端持有鎖。
  • 健壯:無死鎖。即使持有鎖的機器挂了,或者網絡不可達,也不能造成死鎖。
  • 容錯:隻要存在一個可用的鎖平台,那麼就能擷取與釋放鎖。

3. 基于Redis實作鎖的基本原理

實作redis分鎖的最簡單的方法就是在redis中建立一個key,這個key有實效時間,保證分布式鎖的健壯性,保證鎖最終會自動釋放,不會出現死鎖。釋放鎖,就是删除這個key。

上述實作看起來還不錯,但是依然存在問題:

假如獲得鎖的線程在逾時時間内還未處理完成怎麼辦?

假如redis叢集主從複制失敗了怎麼辦?

這兩個問題都會導緻多個線程獲得了鎖,破壞了分布式鎖的安全性。

4. redis實作鎖

為了解決3中的兩個問題,可以随機生成value.

也就是說,在擷取鎖的時候,使用​

​set key value nx px time​

​來保證隻有key不存在時,才會建立,逾時時間是time.逾時時間就是線程持有鎖的最大時間。

釋放鎖的時候,需要驗證目前線程釋放的線程是不是自己持有的鎖。

但是逾時時間問題還是沒有解決。

使用set可以存儲字元串,線程在擷取到鎖後,将擷取鎖的時間做為值放入,同時還要加上線程自己的随機數,将字元串打造成多個屬性的對象的json串。

其他線程在擷取鎖的時候,根據json串,判斷,持有鎖的線程是不是死掉了。

舉個例子:約定持有鎖的線程,每隔1分鐘将json裡面的值++。其他線程嘗試擷取鎖的時候,發現目前時間已經有多個時間間隔的值沒有更新了,那麼就可以認為持有鎖的線程挂了。

5. redis分布式鎖

前面我們考慮的都是單體的redis如何實作分布式鎖。

那麼如果redis也是多個執行個體的,這些執行個體之間完全獨立,沒有主從指派或者其他叢集協調。那麼前面我們讨論的解決方案就不能保證安全了。

為了實作分布式鎖,我們可以約定用戶端嘗試向所有的redis執行個體擷取鎖,如果至少有2/3的redis擷取鎖成功,那麼就表示這個用戶端擷取分布式鎖成功。鎖需要時間戳和随機值保證唯一性。

因為我們的門檻值是2/3,不可能同時有多個線程擷取2/3的鎖,而且這些鎖還是同一把鎖。

為了防止redis執行個體不可達,我們不僅僅需要2/3成功,還需要在擷取鎖的時候,設定小于2個數量級的逾時時間。

舉個例子:

我們redis執行個體有5個,這些redis執行個體之間沒沒有任何關系。

接着用戶端得到鎖的key==(所有的鎖的key相同,value不同)==。

然後用戶端嘗試向所有的redis執行個體注冊鎖。

假設有3個redis執行個體注冊成功,此時用戶端持有鎖。

另一個用戶端在第一個用戶端持有鎖的狀态下,嘗試擷取鎖,那麼,此時至少有2/3的redis執行個體的鎖是占有的,那麼嘗試擷取鎖的線程就無法滿足2/3的這個門檻值了,就無法持有鎖了。

嘗試擷取鎖失敗,需要盡快釋放已經擷取持有鎖的redis執行個體,避免影響下一次擷取鎖。

假設鎖的有效時間是10s,那麼用戶端和redis的連接配接逾時時間應該設定為100ms <= 在兩個數量級以上,否則線程花費80%以上的時間擷取了鎖,然後還沒開始使用呢,就逾時了。

逾時續期可以使用==EXPIRE==進行續期。

這個方法能滿足需要,但是依然不太好,因為嘗試擷取鎖的時候,不是同步的,也就是說,無法在同一時間擷取到全部2/3的鎖。擷取鎖的過程中,也需要花費一定的時間。

是以,鎖的實際使用時間是不确定的,即使有逾時時間,實際可使用的時間也是小于逾時時間的。

而且,還存在一個比較緻命的問題,這些redis執行個體之間存在時鐘漂移。當redis執行個體之間沒有做時鐘同步,那麼因為時鐘漂移問題,會造成鎖的實際使用時間很可能是不确定的,往往小于預期時間。

6. 擷取鎖失敗

當用戶端擷取鎖失敗後,不應該立即重試,一般情況下,如果因為沖突而無法擷取到鎖,那麼失敗後立即重試,幾乎也是失敗的。因為多個用戶端在同一時間搶奪同一個鎖,會造成腦裂。(為了防止腦裂,一般解決方式是采用過半政策。得到支援的數量超過一半才能認為是得到整個叢集的支援)

是以,用戶端在擷取鎖失敗後,應該等待随機的時間,然後在嘗試擷取鎖。

而且還應該注意一點,當擷取失敗後,應該盡可能快的釋放已經擷取到的鎖。否則,在一個逾時時間内,沒有用戶端可以擷取鎖。

還是延續前面的例子:我們有5個用戶端,5個redis執行個體。

第一次:每個用戶端得到了一個redis鎖,但是沒有用戶端擷取的redis鎖的數量超過2/3,所有用戶端擷取鎖失敗。

第二次:因為用戶端等待随機的時間,有2個用戶端擷取到了鎖,另外有一個用戶端擷取到了鎖,其他兩個用戶端沒有擷取到鎖,因為不滿足2/3的政策,擷取失敗。

多次進行後,到達了逾時時間,依然沒有用戶端擷取到鎖,那麼,這個鎖就是低可用性的鎖,特别是随着用戶端的數量的增加,可用性也會下降。

失敗懲罰

某個用戶端嘗試擷取鎖,當得到1/3的鎖後,發現剩餘的鎖都被占用了,此時用戶端無法擷取鎖,需要釋放,結果在釋放一半的時候,網絡中斷了,那麼這個用戶端持有的鎖在逾時時間内就無法釋放了。隻能等到逾時時間到,自動釋放。

此時這些鎖可以認為在這段時間内被懲罰了。

7. 最終

這樣就完美了嗎?

當然不是,我們前面的過半政策是2/3如果更小點呢?

假設現在有100個redis執行個體,我們的門檻值是60%。

因為持續并發,需要增加redis執行個體,于是又增加了100個redis執行個體。

如果在增加的同時,正好有用戶端在擷取鎖,那麼此時,就有可能存在多個用戶端擷取到鎖的問題。

是以,這個過半政策,應該是能夠動态計算的。

  • redis執行個體崩潰造成鎖在一定的時間内不可用

即使這樣,在分布式環境下,存在着各種各樣的問題,比如redis執行個體崩潰,導緻鎖本來是空閑的,但是叢集内的部分redis執行個體崩潰了,在進行重新開機恢複的時候,隻恢複到了鎖持有的狀态,此時如果崩潰的機器數量比較大,就會導緻在這部分崩潰的機器的鎖自動釋放前,沒有任何用戶端可以擷取鎖。

  • 因網絡隔離,造成鎖不安全

假設我們有100個redis執行個體,用戶端A現在已經擷取到了2/3的鎖66個,此時,叢集的鎖是占用狀态。

但是因為動态削減redis執行個體,造成B用戶端在嘗試擷取鎖的時候,擷取了33個鎖,就滿足過半政策了(假設從100 -> 48),此時33剛好是48的2/3,那麼就相當于兩個用戶端都擷取到了鎖。

8. 使用

繼續閱讀