1. 程式人生 > >分散式鎖方案—redlock演算法

分散式鎖方案—redlock演算法

分散式系統的複雜之處在於在不同程序需要互斥的訪問共享資源時的問題。例如,

1、分散式ID,當資料水平拆分之後,如何保證ID的唯一性,並且儘可能的短;

2、秒殺系統中的庫存,資料結構為商品ID,剩餘數量,每次成交會減掉響應數量。如何保證不會超賣;

鎖的目的是確保多個節點、程序做同樣工作的時候,只有一個可以執行成功。有且只有一次。

 

實現分散式鎖有很多方案,例如基於資料庫實現,基於zookeeper實現,如果吞吐量還是不能滿足,比較廣泛的做法是用分散式快取來實現。

一、Redis單節點方式實現

核心就是圍繞SETNX(SETIF NOT EXISTS)實現,

 


key不存在時返回1,當key存在時返回0。因為我們都知道redis是單執行緒的,所以在redis服務側不會有執行緒安全問題。當返回1的時候認為獲得鎖成功,可以進行相應的業務處理,處理完成後,刪除key,釋放鎖,當返回0時表示失敗;

1.超時問題

如果執行緒A拿到了鎖,去處理業務的過程中,發生阻塞,例如資料庫執行比較慢,或者service1發生故障了,這時候如果沒有超時時間,系統將永久的死鎖。所以setnx可以接受第三個引數,也就是超時時間。


這裡面存在問題,因為你並不知道超時的情況下,業務到底有沒有處理成功,還有沒有在繼續進行。所以,這裡的鎖並不是絕對的。

2.如何釋放鎖?

直接del可以嗎?答案是否定的。

這裡要注意另一個問題,因為超時時間是放在redis服務端計算的,如果service1超時了,但是他自己是不知道自己超時的,除非不斷的去輪訓redis確認,不斷輪訓也是有問題的,因為輪訓是有時間差的,例如,你請求redis的時候還沒超時,恰好刪除的時候超時了,service2剛拿到鎖,service1就誤刪了service2的鎖,造成鎖失效。

解決方案就是setnx的時候value值可以在客戶端生成一個隨機值,例如set lock_namerandom_value 。刪除的時候根據key獲取value,如果相同就刪除。當然,這個地方必須是原子的,否則,判斷到刪除之前還是有可能發生變化。可以通過

lua指令碼來實現。

3.單點問題

還有另外一個問題,redis是單點的,如果redis一旦掛掉,整個全玩完。
有人說,可以用master-slave啊,但是master-slave之間是非同步傳輸資料的,也就是不能設定為masterslave都寫成功了才返回。Redis-cluster也是非同步的。

 

二、Redlock實現方案



Redlockredis作者antirez大神在redis官網中給出的一種基於redis的分散式鎖方案。直白點說,就是採用N(通常是5)個獨立的redis節點,同時setnx,如果多數節點成功,就拿到了鎖,這樣就可以允許少數(2)個節點掛掉了。整個取鎖、釋放鎖的操作和單節點類似。

是不是這樣就完美了呢?當然不是。

1.    重啟問題

假設一共有5個節點(A/B/C/D/E),service1成功獲取了鎖,注意這裡,service1A/B/Csetnx成功,但是並沒有在D/E上成功,如果C節點掛掉,又恰巧重啟了,如果C節點並沒有持久化,這時候service2也可以成功鎖住C/D/E,導致鎖失效。

有人說,那C持久化不就行了嗎,實際上設定為同步的持久化方式對效能影響比較大。也就是常說的appendfsync always,如果是機械硬碟,吞吐量可能會從10萬級降到幾百。有人說用固態硬碟不就解決了嗎?土豪是可以用的,吞吐量確實能到萬級,但是大量的、頻繁的寫入容易導致寫入放大。

這個問題的解決方案也非常簡單。就是延遲重啟(elayed restarts),說白了就是等到掛掉節點上的所有鎖都過期了再重啟,重啟後,以前的鎖都已經失效。

2.    響應失敗

如果一個節點獲取鎖成功了,四個節點都setnx成功,一個失敗了,失敗的情況是:發起請求成功,在返回ack的時候失敗,這時候客戶端會認為是失敗了,而刪除鎖的時候沒有刪除這個節點,這就會導致過期之前,這個節點獲取鎖一直失敗,所以,正確的做法是無論setnx成功還是失敗,都應該執行一次刪除操作。

三、總結

看似已經是比較完美的方案了,裡面實際上還有很多問題值得深入思考。另一位大神Martin Kleppmann發起了挑戰,在他的blog中描述了很多漏洞。《Howto do distributed locking》,antirez給予了正面迴應。限於篇幅,下回分解。

四、參考文件

https://redis.io/topics/distlock