Redis實現(xiàn)分布式鎖詳解
一、前言
為什么需要分布式鎖?
在我們的日常開發(fā)中,一個進(jìn)程中當(dāng)多線程的去競爭某一資源的時候,我們通常會用一把鎖來保證只有一個線程獲取到資源。如加上synchronize關(guān)鍵字或ReentrantLock鎖等操作。
那么,如果是多個進(jìn)程相互競爭一個資源,如何保證資源只會被一個操作者持有呢?
例如:微服務(wù)的架構(gòu)下,多個應(yīng)用服務(wù)要同時對同一條數(shù)據(jù)做修改,那么要確保數(shù)據(jù)的正確性,就只能有一個應(yīng)用修改成功。
server1、server2、server3 這三個服務(wù)都要修改amount這個數(shù)據(jù),每個服務(wù)更新的值不同,為了保證數(shù)據(jù)的正確性,三個服務(wù)都向lock server服務(wù)申請修改權(quán)限,最終server2拿到了修改權(quán)限,即server2將amount更新為2,其他服務(wù)由于沒有獲取到修改權(quán)限則返回更新失敗。
二、基于redis實現(xiàn)分布式鎖
為什么redis可以實現(xiàn)分布式鎖?
因為redis是一個單獨的非業(yè)務(wù)服務(wù),不會受到其他業(yè)務(wù)服務(wù)的限制,所有的業(yè)務(wù)服務(wù)都可以向redis發(fā)送寫入命令,且只有一個業(yè)務(wù)服務(wù)可以寫入命令成功,那么這個寫入命令成功的服務(wù)即獲得了鎖,可以進(jìn)行后續(xù)對資源的操作,其他未寫入成功的服務(wù),則進(jìn)行其他處理。
如何實現(xiàn)?
redis的String類型就可以實現(xiàn)。
鎖的獲取
setnx命令:表示SET if Not eXists,即如果 key 不存在,才會設(shè)置它的值,否則什么也不做。
兩個客戶端同時向redis寫入try_lock,客戶端1寫入成功,即獲取分布式鎖成功??蛻舳?寫入失敗,則獲取分布式鎖失敗。
鎖的釋放
當(dāng)客戶端1操作完后,釋放鎖資源,即刪除try_lock。那么此時客戶端2再次嘗試獲取鎖時,則會獲取鎖成功。
那么這樣分布式鎖就這樣結(jié)束了?不不不,現(xiàn)實往往有很多情況出現(xiàn)。
假如客戶端1在獲取到鎖資源后,服務(wù)宕機(jī)了,那么這個try_lock會一直存在redis中,那么其他服務(wù)就永遠(yuǎn)無法獲取到鎖了。
如何解決這個問題呢?
三、如何避免死鎖?鎖的過期時間如何設(shè)置?
避免死鎖
設(shè)置鍵過期時間,超過這個時間即給key刪除掉。
這樣的話,就算當(dāng)前服務(wù)獲取到鎖后宕機(jī)了,這個key也會在一定時間后被刪除,其他服務(wù)照樣可以繼續(xù)獲取鎖。
給serverLock鍵設(shè)置一個10秒的過期時間,10秒后會自動刪除該鍵。
這樣雖然解決了上面說的問題,但是又會引入新的問題。
假如服務(wù)A加鎖成功,鎖會在10s后自動釋放,但由于業(yè)務(wù)復(fù)雜,執(zhí)行時間過長,10s內(nèi)還沒執(zhí)行完,此時鎖已經(jīng)被redis自動釋放掉了。此時服務(wù)B就重新獲取到了該鎖,服務(wù)B開始執(zhí)行他的業(yè)務(wù),服務(wù)A在執(zhí)行到第12s的時候執(zhí)行完了,那么服務(wù)A會去釋放鎖,則此時釋放的卻是服務(wù)B剛獲取到的鎖。
這會有鎖過期和釋放其他服務(wù)鎖這種嚴(yán)重的問題。
鎖過期處理
那么鎖過期這種問題該如何處理的?
雖然可以通過增加刪除key時間來處理這個問題,但是并沒有從根本上解決。假設(shè)設(shè)個100s,絕大多數(shù)都是1s后就會釋放鎖,但是由于服務(wù)宕機(jī),則會導(dǎo)致100s內(nèi)其他服務(wù)都無法獲取到鎖,這也是災(zāi)難性的。
我們可以這樣做,在鎖將要過期的時候,如果服務(wù)還沒有處理完業(yè)務(wù),那么將這個鎖再續(xù)一段時間。比如設(shè)置key在10s后過期,那么再開啟一個守護(hù)線程,在第8s的時候檢測服務(wù)是否處理完,如果沒有,則將這個key再續(xù)10s后過期。
在Redisson(Redis SDK客戶端)中,就已經(jīng)幫我們實現(xiàn)了這個功能,這個自動續(xù)時的我們稱其為”看門狗”。
釋放其他服務(wù)的鎖如何處理呢?
每個服務(wù)在設(shè)置value的時候,帶上自己服務(wù)的唯一標(biāo)識,如UUID,或者一些業(yè)務(wù)上的獨特標(biāo)識。這樣在刪除key的時候,只刪除自己服務(wù)之前添加的key就可以了。
如果需要先查看鎖是否是自己服務(wù)添加的,需要先get取出來判斷,然后再進(jìn)行del。這樣的話就無法保證原子性了。
我們可以通過Lua腳本,將這兩個操作合并成一個操作,就可以保證其原子性了。
Lua腳本的話,我也不會,用到的時候百度就完了。
如果是在單redis實例的情況下,上面的已經(jīng)完全實現(xiàn)了分布式鎖的功能了。
那么redis宕機(jī)了呢?
這個時候就得引入redis集群了。
但是涉及到redis集群,就會有新的問題出現(xiàn),假設(shè)是主從集群,且主從數(shù)據(jù)并不是強(qiáng)一致性。當(dāng)主節(jié)點宕機(jī)后,主節(jié)點的數(shù)據(jù)還未來得及同步到從節(jié)點,進(jìn)行主從切換后,新的主節(jié)點并沒有老的主節(jié)點的全部數(shù)據(jù),這就會導(dǎo)致剛寫入到老的主節(jié)點的鎖在新的主節(jié)點并沒有,其他服務(wù)來獲取鎖時還是會加鎖成功。此時則會有2個服務(wù)都可以操作公共資源,此時的分布式鎖則是不安全的。
redis的作者也想到這個問題,于是他發(fā)明了RedLock。
四、RedLock
什么是RedLock?
要實現(xiàn)RedLock,需要至少5個實例(官方推薦),且每個實例都是master,不需要從庫和哨兵。
實現(xiàn)流程
1、客戶端先獲取當(dāng)前時間戳T1
2、客戶端依次向5個master實例發(fā)起加鎖命令,且每個請求都會設(shè)置超時時間(毫秒級,注意:不是鎖的超時時間),如果某一個master實例由于網(wǎng)絡(luò)等原因?qū)е录渔i失敗,則立即想下一個master實例申請加鎖。
3、當(dāng)客戶端加鎖成功的請求大于等于3個時,且再次獲取當(dāng)前時間戳T2,
當(dāng)時間戳T2 - 時間戳T1 < 鎖的過期時間。則客戶端加鎖成功,否則失敗。
4、加鎖成功,開始操作公共資源,進(jìn)行后續(xù)業(yè)務(wù)操作
5、加鎖失敗,向所有redis節(jié)點發(fā)送鎖釋放命令
即當(dāng)客戶端在大多數(shù)redis實例上申請加鎖成功后,且加鎖總耗時小于鎖過期時間,則認(rèn)為加鎖成功。
釋放鎖需要向全部節(jié)點發(fā)送鎖釋放命令。
第3步為啥要計算申請鎖前后的總耗時與鎖釋放時間進(jìn)行對比呢?
因為如果申請鎖的總耗時已經(jīng)超過了鎖釋放時間,那么可能前面申請redis的鎖已經(jīng)被釋放掉了,保證不了大于等于3個實例都有鎖存在了,鎖也就沒有意義了
這樣的話分布式鎖就真的沒問題了嘛?
1、得5個redis實例,成本大大增加
2、可以通過上面的流程感受到,這個RedLock鎖太重了
3、主從切換這種場景絕大多數(shù)的時候不會碰到,偶爾碰到的話,保證最終的兜底操作我覺得也沒啥問題。
4、分布式系統(tǒng)中的NPC問題
分布式系統(tǒng)中的NPC問題
(可不是游戲里的NPC提問哦)
N:Network Delay,網(wǎng)絡(luò)延遲
P:Process Pause,進(jìn)程暫停(GC)
C:Clock Drift,時鐘漂移
舉個例子吧:
1、客戶端 1 請求鎖定節(jié)點 A、B、C、D、E
2、客戶端 1 的拿到鎖后,進(jìn)入 GC(時間比較久)
3、所有 Redis 節(jié)點上的鎖都過期了
4、客戶端 2 獲取到了 A、B、C、D、E 上的鎖
5、客戶端 1 GC 結(jié)束,認(rèn)為成功獲取鎖
6、客戶端 2 也認(rèn)為獲取到了鎖,發(fā)生【沖突】
在第2步已經(jīng)成功獲取到鎖后,由于GC時間超過鎖過期時間,導(dǎo)致GC完成后其他客戶端也能夠獲取到鎖,此時2個客戶端都會持有鎖。就會有問題。
這個問題無論是redlock還是zookeeper都會有這種問題。不做業(yè)務(wù)上的兜底操作就沒得解。
時鐘漂移問題也只能是盡量避免吧。無法做到根本解決。
個人思考
用RedLock覺得性價比很低。原因如下
1、得額外的多臺服務(wù)器部署redis,每臺服務(wù)器可都是錢啊,而且部署和運(yùn)維的成本也增加了。
2、用RedLock感覺太重了,效率會很低,既然用了redis,就是為了提升效率,結(jié)果一個鎖大大降低了效率
3、如果在集群情況下有鎖丟失的情況,我們業(yè)務(wù)上做好兜底操作就可以了,可以不用上RedLock。
4、畢竟集群情況下主從切換的場景還是極少的,為了極少的情況去浪費(fèi)大量的性能,感覺劃不來
5、就算是上了RedLock,也是避免不了NPC問題的,還不是得業(yè)務(wù)上做兜底。
聊了這么多的redis實現(xiàn)分布式鎖。也簡單了解下zookeeper是如何實現(xiàn)分布式鎖的吧。
五、基于zookeeper實現(xiàn)分布式鎖
什么是zookeeper(zk)?
zk是一個分布式協(xié)調(diào)服務(wù),功能包括:配置維護(hù)、域名服務(wù)、分布式同步、組服務(wù)等。
zk的數(shù)據(jù)結(jié)構(gòu)跟Unix文件系統(tǒng)類似。是一顆樹形結(jié)構(gòu),這里不做詳細(xì)介紹。
zookeeper節(jié)點介紹
zk的節(jié)點稱之為znode節(jié)點,znode節(jié)點分兩種類型:
1、臨時節(jié)點(Ephemeral):當(dāng)客戶端與服務(wù)器斷開連接后,臨時znode節(jié)點就會被自動刪除
2、持久節(jié)點(Persistent):當(dāng)客戶端與服務(wù)器斷開連接后,持久znode節(jié)點不會被自動刪除
znode節(jié)點還有一些特性:
1、節(jié)點有序:在一個父節(jié)點下創(chuàng)建子節(jié)點,zk提供了一個可選的有序性,創(chuàng)建子節(jié)點時會根據(jù)當(dāng)前子節(jié)點數(shù)量給節(jié)點名添加序號。例:/root下創(chuàng)建/java,生成的節(jié)點名稱則為java0001,/root/java0001。
2、臨時節(jié)點:當(dāng)會話結(jié)束或超時,自動刪除節(jié)點
3、事件監(jiān)聽:當(dāng)節(jié)點有創(chuàng)建,刪除,數(shù)據(jù)修改,子節(jié)點變更的時候,zk會通知客戶端的。
zookeeper分布式鎖的實現(xiàn)
zookeeper就是通過臨時節(jié)點和節(jié)點有序來實現(xiàn)分布式鎖的。
1、每個獲取鎖的線程會在zk的某一個目錄下創(chuàng)建一個臨時有序的節(jié)點。
2、節(jié)點創(chuàng)建成功后,判斷當(dāng)前線程創(chuàng)建的節(jié)點的序號是否是最小的。
3、如果序號是最小的,那么獲取鎖成功。
4、如果序號不是最小的,則對他序號的前一個節(jié)點添加事件監(jiān)聽。如果前一個節(jié)點被刪了(鎖被釋放了),那么就會喚醒當(dāng)前節(jié)點,則成功獲取到鎖。
六、zookeepe和redisr兩者的優(yōu)缺
zookeeper
優(yōu)點:
1、不用設(shè)置過期時間
2、事件監(jiān)聽機(jī)制,加鎖失敗后,可以等待鎖釋放
缺點:
1、性能不如redis
2、當(dāng)網(wǎng)絡(luò)不穩(wěn)定時,可能會有多個節(jié)點同時獲取鎖問題。例:node1由于網(wǎng)絡(luò)波動,導(dǎo)致zk將其刪除,剛好node2獲取到鎖,那么此時node1和node2兩者都會獲取到鎖。
Redis
優(yōu)點:性能上比較好,天然的支持高并發(fā)
缺點:
1、獲取鎖失敗后,得輪詢的去獲取鎖
2、大多數(shù)情況下redis無法保證數(shù)據(jù)強(qiáng)一致性
七、那么實際的工作中,該如何選擇呢?
比如我來說,很簡單,沒得選,就Redis,為啥?因為公司沒有用zk。
具體如何選擇,還是得看公司是否有使用相應(yīng)的中間件。
如果兩種公司都有使用,那就具體的看業(yè)務(wù)場景了,看是基于性能考慮還是其他方面的考慮。
如果用redis的話,個人覺得沒必要上RedLock,感覺性價比太低。
但是要注意的是,無論哪一種,在極端的情況下,都會有鎖失效或鎖沖突的情況出現(xiàn),因此業(yè)務(wù)上,設(shè)計上要有兜底的方案,不要造成不必要的損失。
本文中沒有通過代碼來實現(xiàn)分布式鎖,只是提供了方向和思路,以及要注意的地方。至于具體如何通過代碼實現(xiàn),Java的話有Redisson封裝好了大部分功能,使用起來也比較簡單,大家可以參考相應(yīng)的文檔即可。
到此這篇關(guān)于Redis實現(xiàn)分布式鎖詳解的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis官方ORM框架比RedisTemplate更優(yōu)雅
這篇文章主要為大家介紹了Redis官方ORM框架比RedisTemplate更優(yōu)雅的使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07使用Redis獲取數(shù)據(jù)轉(zhuǎn)json,解決動態(tài)泛型傳參的問題
這篇文章主要介紹了使用Redis獲取數(shù)據(jù)轉(zhuǎn)json,解決動態(tài)泛型傳參的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07