為什么需要分布式鎖
在一個(gè)分布式系統(tǒng)中,也會涉及多個(gè)節(jié)點(diǎn)訪問同一個(gè)公共資源的情況,此時(shí)就需要通過鎖來做互斥控制,避免出現(xiàn)類似于“線程安全”的問題,而java的synchronized這樣的鎖只能在當(dāng)前進(jìn)程中生效,在分布式的這種多個(gè)進(jìn)程多個(gè)主機(jī)的場景無能為力,此時(shí)就需要分布式鎖。
分布式鎖的基礎(chǔ)實(shí)現(xiàn)
例如:買票場景,現(xiàn)在車站提供了若干車次,每個(gè)車次的票數(shù)都是固定的。現(xiàn)在又多個(gè)服務(wù)器節(jié)點(diǎn),都可能需要處理這個(gè)買票邏輯,先查詢指定車次的余票,如果余票>0,則設(shè)置余票值-=1.
客戶端1先查詢余票,發(fā)現(xiàn)剩余1張,在即將執(zhí)行1->0過程之前;客戶端2也執(zhí)行查詢余票,發(fā)現(xiàn)也是剩余1張,也會執(zhí)行1->0過程。這就造成1張票賣了給兩個(gè)人,即超賣。
我們可以在上述架構(gòu)中引入redis,作為分布式鎖的管理器。
所謂的分布式鎖,也是一個(gè)/一組單獨(dú)的服務(wù)器程序(如redis),給其他服務(wù)器提供“加鎖”服務(wù)。
買票服務(wù)器,在進(jìn)行買票操作的時(shí)候,需要先加鎖。往redis上設(shè)置一個(gè)特殊的鍵值對key-value,完成上述買票操作,再把這個(gè)key-value刪除掉。其他服務(wù)器也想去買票的時(shí)候,也去redis上嘗試設(shè)置key-value,如果發(fā)現(xiàn)key-value已經(jīng)存在,就認(rèn)為“加鎖失敗”(是放棄/阻塞等待,就看具體實(shí)現(xiàn))。這樣就可以保證,第一個(gè)服務(wù)器在執(zhí)行“查詢->更新"的過程中,第二個(gè)服務(wù)器不會執(zhí)行”查詢“,也就解決了”超賣“問題。
:::success
redis中提供的setnx
操作,正好適合上述場景。即key不存在就設(shè)置,存在則設(shè)置失敗
:::
引入過期時(shí)間
某個(gè)服務(wù)器中加鎖成功后(setnx成功),如果該服務(wù)器意外發(fā)生宕機(jī),就會導(dǎo)致解鎖操作(刪除該key)不能執(zhí)行,就可能引起其他服務(wù)器始終無法獲取到鎖的情況。
在java的多線程編程中,可以把解鎖操作放到finally中,保證解鎖操作一定會被執(zhí)行到。但是這種做法只是針對進(jìn)程內(nèi)的鎖有用(進(jìn)程異常退出,鎖也就隨之銷毀)。而分布式鎖是無效的,服務(wù)器宕機(jī)以后會導(dǎo)致redis上設(shè)置的key無人刪除,也就導(dǎo)致其他服務(wù)器無法獲取到鎖
:::info
引入過期時(shí)間,使用set ex nx
的方式,在設(shè)置鎖的同時(shí)把過期時(shí)間設(shè)置進(jìn)去,一但時(shí)間到了,key就會自動被刪除掉。
:::
注意!此處設(shè)置過期時(shí)間只能使用一個(gè)命令的方式設(shè)置。
如果分開設(shè)置,比如
setnx
之后,再來個(gè)expire
。redis多個(gè)指令之間,無法保證原子性(redis的原子性是只能保證執(zhí)行,不能保證成功)。此時(shí)就可能出現(xiàn)這兩個(gè)命令,一個(gè)執(zhí)行成功,一個(gè)執(zhí)行失敗情況
引入校驗(yàn)id
對于redis中寫入的加鎖鍵值對,其他節(jié)點(diǎn)也是可以刪除的。
比如 服務(wù)器1寫入一個(gè)
001:1
這樣的鍵值對,服務(wù)器2是完全可以把001:1
給刪除掉。當(dāng)然,服務(wù)器2一般不會這樣”惡意刪除“操作,不過不能保證因?yàn)橐恍゜ug導(dǎo)致服務(wù)器2把鎖給誤刪除
為了解決上述問題,我們可以引入一個(gè)校驗(yàn)id。
- 給服務(wù)器編號,每個(gè)服務(wù)器都有一個(gè)自己的身份標(biāo)識
- 進(jìn)行加鎖的時(shí)候,設(shè)置key-value。key是針對哪個(gè)資源加鎖(比如車次),value就可以存儲剛才服務(wù)器的編號,標(biāo)識出當(dāng)前這個(gè)鎖是哪個(gè)服務(wù)器加上的。
- 解鎖的時(shí)候,先查詢一下這個(gè)鎖對應(yīng)的服務(wù)器編號,然后判定一下value是否和當(dāng)前執(zhí)行解鎖的服務(wù)器編號一致,如果一致,才能真正執(zhí)行
del
,如果不是,就失敗。
偽代碼如下:
String key = [要加鎖的資源 id];
String serverId = [服務(wù)器的編號];
// 加鎖, 設(shè)置過期時(shí)間為 10s
redis.set(key, serverId, "NX", "EX", "10s");
// 執(zhí)?各種業(yè)務(wù)邏輯, ?如修改數(shù)據(jù)庫數(shù)據(jù).
doSomeThing();
// 解鎖, 刪除 key. 但是刪除前要檢驗(yàn)下 serverId 是否匹配.
if (redis.get(key) == serverId) {
redis.del(key);
}
但是很明顯,在解鎖的時(shí)候,get
和del
是兩步操作,不是原子的。
引入lua
在服務(wù)器內(nèi)部,可能是多線程的。例如服務(wù)器1中有兩個(gè)線程都在執(zhí)行上述解鎖操作。
在服務(wù)器1中,看起來只是重復(fù)執(zhí)行del
操作,問題不大???但是當(dāng)服務(wù)器2,執(zhí)行加鎖時(shí),就可能出現(xiàn)問題了。
線程A執(zhí)行完del
操作后,線程B執(zhí)行del
操作之前,服務(wù)器2的線程C正好要執(zhí)行加鎖操作。此時(shí)線程A已經(jīng)把鎖刪除了,線程C是能夠加鎖成功的。但是緊接著,線程B就會執(zhí)行del
操作,就會把服務(wù)器2的加鎖操作給解鎖了。雖然del
操作中有引入校驗(yàn)id,但是線程B在get
操作中已經(jīng)通過id校驗(yàn),可以執(zhí)行del
操作,雖然線程C這把鎖的id不同,也能夠解鎖。
使用redis是事務(wù),能夠避免命令之間的插隊(duì)。但是實(shí)踐中往往是使用lua腳本。由于lua語言非常輕量,因此可以內(nèi)嵌到redis中。我們可以使用lua編寫一些邏輯,把這個(gè)腳本上傳到redis服務(wù)器上,然后就可以讓客服端來控制redis執(zhí)行上述腳本。redis執(zhí)行l(wèi)ua腳本的過程,是原子的。并且redis官方也明確說明,lua屬于事務(wù)的替代方案。
使用lua腳本實(shí)現(xiàn)上述解鎖功能:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
引入看門狗(watch dog)
上述方案中仍然存在一個(gè)重要問題,在加鎖的時(shí)候,需要給key設(shè)置過期時(shí)間。過期時(shí)間,設(shè)置多少合適呢?
- 設(shè)置太短,就可能業(yè)務(wù)邏輯還沒執(zhí)行完,就釋放鎖
- 設(shè)置太長,會導(dǎo)致”鎖釋放不及時(shí)“問題
因此更好的方式是”動態(tài)續(xù)約“,這就需要服務(wù)器這邊有一個(gè)專門的線程,負(fù)責(zé)續(xù)約這件事。我們把這個(gè)負(fù)責(zé)的線程,叫做”看門狗“(watch dog).
舉個(gè)具體的例子:
初始情況下設(shè)置過期時(shí)間10s,同時(shí)設(shè)定看門狗線程每隔3s檢測一次。
當(dāng)3s時(shí)間到的時(shí)候,看門狗就會判定當(dāng)前任務(wù)是否完成。
- 如果任務(wù)已經(jīng)完成,直接通過lua腳本的方式,釋放鎖(刪除key)
- 如果任務(wù)未完成,則把過期時(shí)間重新設(shè)置為10s,即續(xù)約
這樣就不用擔(dān)心鎖提前釋放的問題了,而且另外一方面,如果服務(wù)器掛了,看門狗線程也會被銷毀,此時(shí)無人續(xù)約,這個(gè)key自然就可以迅速過期,讓其他服務(wù)器獲取到鎖
引入redlock算法
實(shí)踐中的redis一般使用集群的方式部署的,那么就可能出現(xiàn)以下比較極端的情況。
服務(wù)器1向master節(jié)點(diǎn)進(jìn)行加鎖操作,這個(gè)寫入key的過程剛完成,master掛了;slave節(jié)點(diǎn)升級成新的master節(jié)點(diǎn),但是由于剛才寫入的這個(gè)key未來得及同步給slave,此時(shí)就相當(dāng)于服務(wù)器1的加鎖操作形同虛設(shè)。服務(wù)器2仍然可以進(jìn)行加鎖,即給新的master寫入key,因?yàn)樾碌膍aster不包含剛才的key。文章來源:http://www.zghlxwxcb.cn/news/detail-730373.html
為了解決這個(gè)問題,redis作者提出了redlock算法。本質(zhì)上是使用冗余解決可用性問題
此處加鎖,就是按照一定的順序,針對redis集群的所有分片都進(jìn)行加鎖操作。如果某個(gè)節(jié)點(diǎn)掛了(加不上鎖了)繼續(xù)給下一個(gè)節(jié)點(diǎn)加鎖即可。如果寫入key成功的節(jié)點(diǎn)個(gè)數(shù)超過總數(shù)的一半,就視為加鎖成功。同理,進(jìn)行解鎖的時(shí)候,也就會把上述節(jié)點(diǎn)都設(shè)置一遍解鎖。文章來源地址http://www.zghlxwxcb.cn/news/detail-730373.html
到了這里,關(guān)于使用redis實(shí)現(xiàn)分布式鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!