新聞中心
Redis應(yīng)用篇(眾星追月):分布式鎖
作者: 小龍coding 2021-10-26 19:37:15
存儲(chǔ)
存儲(chǔ)軟件
分布式
Redis 由于分布式系統(tǒng)「多線程」、「多進(jìn)程」并且「分布在不同機(jī)器」上,這將使原單機(jī)并發(fā)控制鎖策略失效,為了解決這個(gè)問(wèn)題就需要一種「跨JVM的互斥機(jī)制」來(lái)控制共享資源的訪問(wèn),這就得靠分布式鎖啦。

站在用戶(hù)的角度思考問(wèn)題,與客戶(hù)深入溝通,找到澄海網(wǎng)站設(shè)計(jì)與澄海網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶(hù)體驗(yàn)好的作品,建站類(lèi)型包括:成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、域名注冊(cè)、虛擬主機(jī)、企業(yè)郵箱。業(yè)務(wù)覆蓋澄海地區(qū)。
話(huà)題引入
大家好,我是小龍。
之前在《吃透Redis系列》專(zhuān)欄發(fā)表了第一篇文章《Redis基礎(chǔ)篇(萬(wàn)丈高樓平地起):核心底層數(shù)據(jù)結(jié)構(gòu)》簡(jiǎn)單介紹了Redis,以及它的內(nèi)部組織形式、核心數(shù)據(jù)結(jié)構(gòu)與大致使用場(chǎng)景。還沒(méi)看到得同學(xué)可以回過(guò)頭看看。
接下來(lái),我將繼續(xù)帶大家深入理解,本文將介紹Redis高頻使用的一個(gè)場(chǎng)景——「利用Redis實(shí)習(xí)分布式鎖」。
想必大家都知道,在遇到并發(fā)問(wèn)題時(shí),我們通常會(huì)使用鎖來(lái)解決并發(fā)問(wèn)題。
這是,有同學(xué)可能說(shuō):“這個(gè)我會(huì),不就用synchronized、Lock這些實(shí)現(xiàn)嗎?”
對(duì),你說(shuō)的不錯(cuò)。但是你只說(shuō)對(duì)了一半,在「?jìng)鹘y(tǒng)單機(jī)部署」的情況下,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLcok或synchronized)進(jìn)行互斥控制。
但是在「分布式系統(tǒng)」中,由于分布式系統(tǒng)「多線程」、「多進(jìn)程」并且「分布在不同機(jī)器」上,這將使原單機(jī)并發(fā)控制鎖策略失效,為了解決這個(gè)問(wèn)題就需要一種「跨JVM的互斥機(jī)制」來(lái)控制共享資源的訪問(wèn),這就得靠分布式鎖啦。
看透鎖本質(zhì)
在我看來(lái):所有的鎖本身都可以用一個(gè)變量來(lái)表示。
比如:在「單機(jī)上運(yùn)行」的多線程程序來(lái)說(shuō)。取一個(gè)變量,變量為0時(shí),表示沒(méi)有線程獲取鎖;變量為1時(shí),表示已經(jīng)有線程獲取鎖。
加鎖:線程調(diào)用加鎖操作,檢查變量是否為0,如果為0,表示沒(méi)線程獲取鎖,將變量設(shè)置為1,表示獲取鎖;如果不是0,表示其他線程已經(jīng)暫用鎖,獲取鎖失敗。
解鎖:同理。
而分布式環(huán)境下,同樣可以以變量形式理解分布式鎖。
但是,和線程在單機(jī)上操作鎖不同的是,在分布式場(chǎng)景下,「鎖變量需要由一個(gè)共享存儲(chǔ)系統(tǒng)來(lái)維護(hù)」,只有這樣,多個(gè)客戶(hù)端才可以通過(guò)訪問(wèn)共享存儲(chǔ)系統(tǒng)來(lái)訪問(wèn)鎖變量。相應(yīng)的,「加鎖和釋放鎖的操作就變成了讀取、判斷和設(shè)置共享存儲(chǔ)系統(tǒng)中的鎖變量值」。
「可見(jiàn),滿(mǎn)足分布式鎖的要求」:
- 「鎖操作原子性」:分布式鎖的加鎖和釋放鎖的過(guò)程,涉及多個(gè)操作。所以,在實(shí)現(xiàn)分布式鎖時(shí),我們需要保證這些「鎖操作的原子性」;
- 「鎖的可靠性」:共享存儲(chǔ)系統(tǒng)保存了鎖變量,如果共享存儲(chǔ)系統(tǒng)發(fā)生故障或宕機(jī),那么客戶(hù)端也就無(wú)法進(jìn)行鎖操作了。在實(shí)現(xiàn)分布式鎖時(shí),我們需要考慮保證「共享存儲(chǔ)系統(tǒng)的可靠性」,進(jìn)而保證「鎖的可靠性」。
上面我們提到了可以使用一個(gè)鎖變量來(lái)表示鎖,其實(shí)你也可以理解為「占位」。只不過(guò)分布式鎖需要把這個(gè)坑位拿出來(lái)放于「共享」的地方,每個(gè)都從「共享處來(lái)檢查坑位」。
占位一般是使用 setnx(set if not exists) 指令,只允許被一個(gè)客戶(hù)端占位。先來(lái)先占, 用完了,再調(diào)用 del 指令釋放茅坑。
- //加鎖
- > setnx lock_key 1
- OK
- //業(yè)務(wù)邏輯
- >(其他操作)
- //釋放鎖
- > del lock_key
但是有個(gè)問(wèn)題,如果邏輯執(zhí)行到中間出現(xiàn)異常了,可能會(huì)導(dǎo)致 del 指令沒(méi)有被調(diào)用,這樣就會(huì)「陷入死鎖」,鎖永遠(yuǎn)得不到釋放。
于是我們?cè)谀玫芥i之后,再給鎖加上一個(gè)過(guò)期時(shí)間,這樣即使中間出現(xiàn)異常也可以保證指定時(shí)間之后鎖會(huì)自動(dòng)釋放。
- //加鎖
- > setnx lock_key 1
- OK
- > expire lock_key 5
- //業(yè)務(wù)邏輯
- >(其他操作)
- //釋放鎖
- > del lock_key
但是以上邏輯還有問(wèn)題。如果在 setnx 和 expire 之間服務(wù)器進(jìn)程突然掛掉了,可能是因?yàn)闄C(jī)器掉電或者是被人為殺掉的,就會(huì)導(dǎo)致 expire 得不到執(zhí)行,也會(huì)造成死鎖。
這種問(wèn)題的根源就在于 setnx 和 expire 是兩條指令而不是原子指令。你也許會(huì)想到使用事務(wù)什么的執(zhí)行,但是這里不行,因?yàn)槿绻?setnx 沒(méi)搶到鎖,expire 是不應(yīng)該執(zhí)行的。
Redis 2.8 版本中作者加入了 set 指令的擴(kuò)展參數(shù),使得 setnx 和expire 指令可以一起執(zhí)行,徹底解決了分布式鎖的亂象。
- set key value [EX seconds | PX milliseconds] [NX]
除了上述基本常規(guī)的問(wèn)題,還有這些「你可能沒(méi)考慮到的問(wèn)題」:
超時(shí)問(wèn)題
Redis 的分布式鎖不能解決超時(shí)問(wèn)題,如果在加鎖和釋放鎖間的業(yè)務(wù)邏輯執(zhí)行時(shí)間太長(zhǎng),以至于超出了鎖的超時(shí)限制,就會(huì)出現(xiàn)問(wèn)題(也就是鎖過(guò)期了,你的業(yè)務(wù)邏輯還沒(méi)執(zhí)行完)。
因?yàn)檫@時(shí)候鎖過(guò)期了,第二個(gè)客戶(hù)端B重新持有了這把鎖,但是緊接著客戶(hù)端A執(zhí)行完了業(yè)務(wù)邏輯,就把鎖給釋放了,客戶(hù)端C就會(huì)在客戶(hù)端B邏輯執(zhí)行完之間拿到了鎖。為了避免這個(gè)問(wèn)題,Redis 分布式鎖不要用于較長(zhǎng)時(shí)間的任務(wù)。
為了應(yīng)對(duì)這個(gè)問(wèn)題,我們需要能區(qū)分來(lái)自不同客戶(hù)端的鎖操作,具體咋做呢 ? 針對(duì)于這個(gè)問(wèn)題,我們可以想辦法把命令略加點(diǎn)小技巧。可以在鎖變量的值上想想辦法。
在使用SETNX命令進(jìn)行加鎖的方法中,我們通過(guò)把鎖變量值設(shè)置為1或0,表示是否加鎖成功。1和0只有兩種狀態(tài),無(wú)法表示究竟是哪個(gè)客戶(hù)端進(jìn)行的鎖操作。
所以,我們?cè)诩渔i操作時(shí),可以「讓每個(gè)客戶(hù)端給鎖變量設(shè)置一個(gè)唯一值」,這里的唯一值就可以用來(lái)標(biāo)識(shí)當(dāng)前操作的客戶(hù)端。
在釋放鎖操作時(shí),客戶(hù)端需要判斷,當(dāng)前「鎖變量的值是否和自己的唯一標(biāo)識(shí)相等」,只有在相等的情況下,才能釋放鎖。這樣一來(lái),就不會(huì)出現(xiàn)誤釋放鎖的問(wèn)題了。
于是,我們的命令可以這樣寫(xiě):
- //加鎖,unique_value作為客戶(hù)端唯—性的標(biāo)識(shí)
- SET lock_key unique_value NX PX 5000
其中,unique_value 是客戶(hù)端的唯一標(biāo)識(shí),可以用一個(gè)隨機(jī)生成的字符串來(lái)表示,PX 5000則表示 lock_key會(huì)在5s后過(guò)期,以免客戶(hù)端在這期間發(fā)生異常而無(wú)法釋放鎖。
因?yàn)樵诩渔i操作中,每個(gè)客戶(hù)端都使用了一個(gè)唯一標(biāo)識(shí),所以在「釋放鎖操作」時(shí),我們需要「判斷鎖變量的值」,是否等于執(zhí)行釋放鎖操作的客戶(hù)端的唯一標(biāo)識(shí),如下所示,可以使用Lua腳本來(lái)保證原子性:
- //釋放鎖比較unique_value是否相等,避免誤釋放
- if redis.call("get" ,KEYS[1])== ARGV[1] then
- return redis.call("del" , KEYS[1])
- else
- return 0
- end
可重入性
可重入性是指線程在持有鎖的情況下再次請(qǐng)求加鎖,如果一個(gè)鎖支持同一個(gè)線程的多次加鎖,那么這個(gè)鎖就是可重入的。比如 Java 語(yǔ)言里有個(gè) ReentrantLock 就是可重入鎖。
Redis 分布式鎖如果要支持可重入,可以對(duì)客戶(hù)端的 set 方法進(jìn)行包裝,使用線程的 Threadlocal 變量存儲(chǔ)當(dāng)前持有鎖的計(jì)數(shù)。
此處就不過(guò)多介紹,大抵不會(huì)問(wèn),有興趣可以自己上網(wǎng)查閱看書(shū)。
課外補(bǔ)充
上述內(nèi)容,是個(gè)基于單個(gè)Redis節(jié)點(diǎn)實(shí)現(xiàn)分布式鎖。
當(dāng)我們要實(shí)現(xiàn)「高可靠的分布式鎖」時(shí),就不能只依賴(lài)單個(gè)的命令操作了,我們需要按照一定的步驟和規(guī)則進(jìn)行加解鎖操作,否則,就可能會(huì)出現(xiàn)鎖無(wú)法工作的情況?!耙欢ǖ牟襟E和規(guī)則”是指啥呢?其實(shí)就是分布式鎖的算法。
這里簡(jiǎn)單介紹Redlock算法的執(zhí)行步驟。Redlock算法的實(shí)現(xiàn)需要有N個(gè)獨(dú)立的Redis實(shí)例。接下來(lái),我們可以分成3步來(lái)完成加鎖操作。
1、客戶(hù)端獲取當(dāng)前時(shí)間
2、客戶(hù)端按照順序在每個(gè)Master實(shí)例中嘗試獲得鎖。在獲得鎖的過(guò)程中,為每一個(gè)鎖操作設(shè)置一個(gè)快速失敗時(shí)間(如果想要獲得一個(gè)10秒的鎖,那么每一個(gè)鎖操作的失敗時(shí)間設(shè)為5-50ms)。
這樣可以避免客戶(hù)端與一個(gè)已經(jīng)故障的Master通信占用太長(zhǎng)時(shí)間,通過(guò)快速失敗的方式盡快的與集群中的其他節(jié)點(diǎn)完成鎖操作。
3、客戶(hù)端計(jì)算出與master獲得鎖操作過(guò)程中消耗的時(shí)間,「當(dāng)且僅當(dāng)Client獲得鎖消耗的時(shí)間小于鎖的存活時(shí)間,并且在一半以上的master節(jié)點(diǎn)中獲得鎖」。才認(rèn)為client成功的獲得了鎖。
4、如果已經(jīng)獲得了鎖,「Client執(zhí)行任務(wù)的時(shí)間窗口是鎖的存活時(shí)間減去獲得鎖消耗的時(shí)間?!?/p>
5、如果Client獲得鎖的數(shù)量不足一半以上,或獲得鎖的時(shí)間超時(shí),那么認(rèn)為獲得鎖失敗??蛻?hù)端「需要嘗試在所有的master節(jié)點(diǎn)中釋放鎖, 即使在第二步中沒(méi)有成功獲得該Master節(jié)點(diǎn)中的鎖,仍要進(jìn)行釋放操作?!?/p>
名稱(chēng)欄目:Redis應(yīng)用篇(眾星追月):分布式鎖
本文來(lái)源:http://m.fisionsoft.com.cn/article/coshsio.html


咨詢(xún)
建站咨詢(xún)
