Redis實現分佈式鎖詳解

一、前言

為什麼需要分佈式鎖?

在我們的日常開發中,一個進程中當多線程的去競爭某一資源的時候,我們通常會用一把鎖來保證隻有一個線程獲取到資源。如加上synchronize關鍵字或ReentrantLock鎖等操作。

那麼,如果是多個進程相互競爭一個資源,如何保證資源隻會被一個操作者持有呢?

例如:微服務的架構下,多個應用服務要同時對同一條數據做修改,那麼要確保數據的正確性,就隻能有一個應用修改成功。

server1、server2、server3 這三個服務都要修改amount這個數據,每個服務更新的值不同,為瞭保證數據的正確性,三個服務都向lock server服務申請修改權限,最終server2拿到瞭修改權限,即server2將amount更新為2,其他服務由於沒有獲取到修改權限則返回更新失敗。

二、基於redis實現分佈式鎖

為什麼redis可以實現分佈式鎖?

因為redis是一個單獨的非業務服務,不會受到其他業務服務的限制,所有的業務服務都可以向redis發送寫入命令,且隻有一個業務服務可以寫入命令成功,那麼這個寫入命令成功的服務即獲得瞭鎖,可以進行後續對資源的操作,其他未寫入成功的服務,則進行其他處理。

如何實現?

redis的String類型就可以實現。

鎖的獲取

setnx命令:表示SET if Not eXists,即如果 key 不存在,才會設置它的值,否則什麼也不做。

兩個客戶端同時向redis寫入try_lock,客戶端1寫入成功,即獲取分佈式鎖成功。客戶端2寫入失敗,則獲取分佈式鎖失敗。

鎖的釋放

當客戶端1操作完後,釋放鎖資源,即刪除try_lock。那麼此時客戶端2再次嘗試獲取鎖時,則會獲取鎖成功。

那麼這樣分佈式鎖就這樣結束瞭?不不不,現實往往有很多情況出現。

假如客戶端1在獲取到鎖資源後,服務宕機瞭,那麼這個try_lock會一直存在redis中,那麼其他服務就永遠無法獲取到鎖瞭。

如何解決這個問題呢?

三、如何避免死鎖?鎖的過期時間如何設置?

避免死鎖

設置鍵過期時間,超過這個時間即給key刪除掉。

這樣的話,就算當前服務獲取到鎖後宕機瞭,這個key也會在一定時間後被刪除,其他服務照樣可以繼續獲取鎖。

給serverLock鍵設置一個10秒的過期時間,10秒後會自動刪除該鍵。

這樣雖然解決瞭上面說的問題,但是又會引入新的問題。

假如服務A加鎖成功,鎖會在10s後自動釋放,但由於業務復雜,執行時間過長,10s內還沒執行完,此時鎖已經被redis自動釋放掉瞭。此時服務B就重新獲取到瞭該鎖,服務B開始執行他的業務,服務A在執行到第12s的時候執行完瞭,那麼服務A會去釋放鎖,則此時釋放的卻是服務B剛獲取到的鎖。

這會有鎖過期和釋放其他服務鎖這種嚴重的問題。

鎖過期處理

那麼鎖過期這種問題該如何處理的?

雖然可以通過增加刪除key時間來處理這個問題,但是並沒有從根本上解決。假設設個100s,絕大多數都是1s後就會釋放鎖,但是由於服務宕機,則會導致100s內其他服務都無法獲取到鎖,這也是災難性的。

我們可以這樣做,在鎖將要過期的時候,如果服務還沒有處理完業務,那麼將這個鎖再續一段時間。比如設置key在10s後過期,那麼再開啟一個守護線程,在第8s的時候檢測服務是否處理完,如果沒有,則將這個key再續10s後過期。

在Redisson(Redis SDK客戶端)中,就已經幫我們實現瞭這個功能,這個自動續時的我們稱其為”看門狗”。

釋放其他服務的鎖如何處理呢?

每個服務在設置value的時候,帶上自己服務的唯一標識,如UUID,或者一些業務上的獨特標識。這樣在刪除key的時候,隻刪除自己服務之前添加的key就可以瞭。

如果需要先查看鎖是否是自己服務添加的,需要先get取出來判斷,然後再進行del。這樣的話就無法保證原子性瞭。

我們可以通過Lua腳本,將這兩個操作合並成一個操作,就可以保證其原子性瞭。

Lua腳本的話,我也不會,用到的時候百度就完瞭。

如果是在單redis實例的情況下,上面的已經完全實現瞭分佈式鎖的功能瞭。

那麼redis宕機瞭呢?

這個時候就得引入redis集群瞭。

但是涉及到redis集群,就會有新的問題出現,假設是主從集群,且主從數據並不是強一致性。當主節點宕機後,主節點的數據還未來得及同步到從節點,進行主從切換後,新的主節點並沒有老的主節點的全部數據,這就會導致剛寫入到老的主節點的鎖在新的主節點並沒有,其他服務來獲取鎖時還是會加鎖成功。此時則會有2個服務都可以操作公共資源,此時的分佈式鎖則是不安全的。

redis的作者也想到這個問題,於是他發明瞭RedLock。

四、RedLock

什麼是RedLock?

要實現RedLock,需要至少5個實例(官方推薦),且每個實例都是master,不需要從庫和哨兵。

實現流程

1、客戶端先獲取當前時間戳T1

2、客戶端依次向5個master實例發起加鎖命令,且每個請求都會設置超時時間(毫秒級,註意:不是鎖的超時時間),如果某一個master實例由於網絡等原因導致加鎖失敗,則立即想下一個master實例申請加鎖。

3、當客戶端加鎖成功的請求大於等於3個時,且再次獲取當前時間戳T2,

當時間戳T2 – 時間戳T1 < 鎖的過期時間。則客戶端加鎖成功,否則失敗。

4、加鎖成功,開始操作公共資源,進行後續業務操作

5、加鎖失敗,向所有redis節點發送鎖釋放命令

即當客戶端在大多數redis實例上申請加鎖成功後,且加鎖總耗時小於鎖過期時間,則認為加鎖成功。 

 釋放鎖需要向全部節點發送鎖釋放命令。

第3步為啥要計算申請鎖前後的總耗時與鎖釋放時間進行對比呢?

因為如果申請鎖的總耗時已經超過瞭鎖釋放時間,那麼可能前面申請redis的鎖已經被釋放掉瞭,保證不瞭大於等於3個實例都有鎖存在瞭,鎖也就沒有意義瞭

這樣的話分佈式鎖就真的沒問題瞭嘛?

1、得5個redis實例,成本大大增加

2、可以通過上面的流程感受到,這個RedLock鎖太重瞭

3、主從切換這種場景絕大多數的時候不會碰到,偶爾碰到的話,保證最終的兜底操作我覺得也沒啥問題。

4、分佈式系統中的NPC問題

分佈式系統中的NPC問題

(可不是遊戲裡的NPC提問哦)

N:Network Delay,網絡延遲

P:Process Pause,進程暫停(GC)

C:Clock Drift,時鐘漂移

舉個例子吧:

1、客戶端 1 請求鎖定節點 A、B、C、D、E

2、客戶端 1 的拿到鎖後,進入 GC(時間比較久)

3、所有 Redis 節點上的鎖都過期瞭

4、客戶端 2 獲取到瞭 A、B、C、D、E 上的鎖

5、客戶端 1 GC 結束,認為成功獲取鎖

6、客戶端 2 也認為獲取到瞭鎖,發生【沖突】

在第2步已經成功獲取到鎖後,由於GC時間超過鎖過期時間,導致GC完成後其他客戶端也能夠獲取到鎖,此時2個客戶端都會持有鎖。就會有問題。

這個問題無論是redlock還是zookeeper都會有這種問題。不做業務上的兜底操作就沒得解。

時鐘漂移問題也隻能是盡量避免吧。無法做到根本解決。

個人思考

用RedLock覺得性價比很低。原因如下

1、得額外的多臺服務器部署redis,每臺服務器可都是錢啊,而且部署和運維的成本也增加瞭。

2、用RedLock感覺太重瞭,效率會很低,既然用瞭redis,就是為瞭提升效率,結果一個鎖大大降低瞭效率

3、如果在集群情況下有鎖丟失的情況,我們業務上做好兜底操作就可以瞭,可以不用上RedLock。

4、畢竟集群情況下主從切換的場景還是極少的,為瞭極少的情況去浪費大量的性能,感覺劃不來

5、就算是上瞭RedLock,也是避免不瞭NPC問題的,還不是得業務上做兜底。

聊瞭這麼多的redis實現分佈式鎖。也簡單瞭解下zookeeper是如何實現分佈式鎖的吧。

五、基於zookeeper實現分佈式鎖

什麼是zookeeper(zk)?

zk是一個分佈式協調服務,功能包括:配置維護、域名服務、分佈式同步、組服務等。

zk的數據結構跟Unix文件系統類似。是一顆樹形結構,這裡不做詳細介紹。

zookeeper節點介紹

zk的節點稱之為znode節點,znode節點分兩種類型:

1、臨時節點(Ephemeral):當客戶端與服務器斷開連接後,臨時znode節點就會被自動刪除

2、持久節點(Persistent):當客戶端與服務器斷開連接後,持久znode節點不會被自動刪除

znode節點還有一些特性:

1、節點有序:在一個父節點下創建子節點,zk提供瞭一個可選的有序性,創建子節點時會根據當前子節點數量給節點名添加序號。例:/root下創建/java,生成的節點名稱則為java0001,/root/java0001。

2、臨時節點:當會話結束或超時,自動刪除節點

3、事件監聽:當節點有創建,刪除,數據修改,子節點變更的時候,zk會通知客戶端的。

zookeeper分佈式鎖的實現

zookeeper就是通過臨時節點和節點有序來實現分佈式鎖的。

1、每個獲取鎖的線程會在zk的某一個目錄下創建一個臨時有序的節點。

2、節點創建成功後,判斷當前線程創建的節點的序號是否是最小的。

3、如果序號是最小的,那麼獲取鎖成功。

 4、如果序號不是最小的,則對他序號的前一個節點添加事件監聽。如果前一個節點被刪瞭(鎖被釋放瞭),那麼就會喚醒當前節點,則成功獲取到鎖。

六、zookeepe和redisr兩者的優缺

zookeeper

優點:

1、不用設置過期時間

2、事件監聽機制,加鎖失敗後,可以等待鎖釋放

缺點:

1、性能不如redis

2、當網絡不穩定時,可能會有多個節點同時獲取鎖問題。例:node1由於網絡波動,導致zk將其刪除,剛好node2獲取到鎖,那麼此時node1和node2兩者都會獲取到鎖。

Redis

優點:性能上比較好,天然的支持高並發

缺點:

1、獲取鎖失敗後,得輪詢的去獲取鎖

2、大多數情況下redis無法保證數據強一致性

七、那麼實際的工作中,該如何選擇呢?

比如我來說,很簡單,沒得選,就Redis,為啥?因為公司沒有用zk。

具體如何選擇,還是得看公司是否有使用相應的中間件。

如果兩種公司都有使用,那就具體的看業務場景瞭,看是基於性能考慮還是其他方面的考慮。

如果用redis的話,個人覺得沒必要上RedLock,感覺性價比太低。

但是要註意的是,無論哪一種,在極端的情況下,都會有鎖失效或鎖沖突的情況出現,因此業務上,設計上要有兜底的方案,不要造成不必要的損失。

本文中沒有通過代碼來實現分佈式鎖,隻是提供瞭方向和思路,以及要註意的地方。至於具體如何通過代碼實現,Java的話有Redisson封裝好瞭大部分功能,使用起來也比較簡單,大傢可以參考相應的文檔即可。

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

推薦閱讀: