redis分布式鎖和2個(gè)房客的故事你聽過嗎
為什么需要分布式鎖這里就不贅述了。常見的分布式鎖實(shí)現(xiàn)方案有Redis、Zookeeper,數(shù)據(jù)庫(kù)。
設(shè)計(jì)一個(gè)分布式鎖,至少應(yīng)該保證以下3個(gè)方面:
安全: 獨(dú)享(相互排斥)。在任意一個(gè)時(shí)刻,只有一個(gè)客戶端持有鎖。 無死鎖:即便是天塌下來,也要鎖能釋放。 容錯(cuò)。只要大部分Redis節(jié)點(diǎn)都活著,客戶端就可以獲取和釋放鎖。
redis部署方案一般有這3種。
單機(jī)模式
master-slave + sentinel
redis cluster模式
我們看這3種如何實(shí)現(xiàn)分布式鎖。
單機(jī)模式:正常情況下,單機(jī)模式?jīng)]什么大問題,就怕萬(wàn)一redis掛了就完蛋了。一般我們不會(huì)采用單機(jī)版,這里之所以提到它,是因?yàn)閱螜C(jī)版的鎖是其他方案的基礎(chǔ)。redis鎖的命令是:
SET?resource_name?my_value?NX?PX?ms?
//NX是指如果key不存在就就返回true,key存在返回false,PX可以指定過期時(shí)間
如果簡(jiǎn)單的setnx,有些場(chǎng)景下會(huì)有問題。
《倆個(gè)房客的故事》 long long a ago,有A、B倆個(gè)房客前往同一家酒店,又都看上了同一間房,但是一間房同時(shí)只能容納一個(gè)人,經(jīng)過一番舌槍唇戰(zhàn),A搶到了第一次。A預(yù)計(jì)自己半個(gè)小時(shí)能完事,于是就開了半個(gè)小時(shí)的鐘點(diǎn)房。可是A這次發(fā)揮超常,30分鐘還沒完事,但是由于30分鐘時(shí)間已經(jīng)到了,房間鎖自動(dòng)打開(房間空置狀態(tài)),可以接下一位客人。這時(shí)B來了,進(jìn)入房間,鎖上門。正準(zhǔn)備干事的時(shí)候,A干完事出去的時(shí)候把門打開了(釋放鎖),房間變成空置狀態(tài)。A和B都很尷尬。
怎么辦?為了不讓B打擾,A想到了一個(gè)辦法,讓秘書A1每隔10分鐘就把鐘點(diǎn)房的時(shí)間重新設(shè)置成30分鐘。等A事干完了再開門(釋放鎖)。
為了避免A開了B上的鎖,酒店想出了一個(gè)辦法,指紋上鎖,開鎖的時(shí)候也必須用這個(gè)指紋。這樣A就開不了B上的鎖。
回到程序,比如A來setnx,默認(rèn)過期時(shí)間30秒,獲取到鎖,但是A比較墨跡,鎖過期(自動(dòng)釋放鎖)的時(shí)候還在執(zhí)行,這時(shí)候B獲取鎖。等A執(zhí)行完來釋放鎖的時(shí)候,其實(shí)釋放的是B的鎖。這個(gè)時(shí)候就需要多一層判斷。A和B,set的my_value必須不一樣(參考故事中的指紋)。當(dāng)A釋放鎖的時(shí)候先判斷是否是自己的鎖,如果是自己的鎖再釋放。可以通過以下Lua腳本實(shí)現(xiàn):
if?redis.call("get",KEYS[1])?==?ARGV[1]?then
????return?redis.call("del",KEYS[1])
else
????return?0
end
也可以在自己的業(yè)務(wù)中實(shí)現(xiàn),關(guān)鍵點(diǎn)就是my_value必須唯一,能區(qū)分開A和B。
至于A的問題,我們另起一個(gè)線程,來監(jiān)控A的過期時(shí)間,每隔10秒鐘就把A的鎖過期時(shí)間設(shè)置成30秒,直到A釋放鎖。
主從模式,單機(jī)版有單點(diǎn)故障,那master-slave應(yīng)該沒問題了吧。master掛了slave頂上。但是請(qǐng)注意,master與slave之間數(shù)據(jù)同步是異步的。就是說master掛了的時(shí)候,可能有寫數(shù)據(jù)并沒有同步到slave。這時(shí)slave成為master的時(shí)候還是丟了鎖。比如A獲取到某資源的鎖,這時(shí)master掛了,恰巧鎖還沒同步到slave。這時(shí)slave晉升為master的時(shí)候并沒有A的鎖,這時(shí)B過來獲取資源鎖的時(shí)候就成功。安全性得不到保障。當(dāng)然如果訪問量小這個(gè)模式完全夠了,哪有那么巧的事。即便是正好趕上,由于訪問量小,彌補(bǔ)也比較容易。
如果訪問量大,對(duì)安全要求高,就得另尋出路。redis官方給出了一個(gè)解決算法。就是Redlock算法。它要求有N組(臺(tái))redis互相獨(dú)立的節(jié)點(diǎn),它們互相獨(dú)立,沒有主從,也沒有集群。客戶端要做的是(以5臺(tái)服務(wù)為例):
1. 獲取當(dāng)前Unix時(shí)間,以毫秒為單位。
2. 依次嘗試從N個(gè)實(shí)例,使用相同的key和隨機(jī)值獲取鎖。當(dāng)然向Redis設(shè)置鎖時(shí),客戶端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動(dòng)失效時(shí)間為10秒,則超時(shí)時(shí)間應(yīng)該在5-50毫秒之間。這樣可以避免服務(wù)器端Redis已經(jīng)掛掉的情況下,客戶端還在死死地等待響應(yīng)結(jié)果。如果服務(wù)器端沒有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試另外一個(gè)Redis實(shí)例。
3. 客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(步驟1記錄的時(shí)間)就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1 )的Redis節(jié)點(diǎn)都取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。
4. 如果取到了鎖,key的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果)。
5. 如果因?yàn)槟承┰颍@取鎖失敗(沒有在至少N/2+1個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖。
上面這段來源redis中文網(wǎng)。我總結(jié)的大概流程是
依次嘗試從N個(gè)實(shí)例獲取鎖,注意的是獲取鎖的時(shí)間要遠(yuǎn)小于鎖超時(shí)的時(shí)間。 當(dāng)且僅當(dāng)從大多數(shù)(N/2+1 )的Redis節(jié)點(diǎn)都取到鎖才算成功,否則就是失敗。
只有當(dāng)N/2+1個(gè)節(jié)點(diǎn)取到鎖才是算成功。釋放鎖比較簡(jiǎn)單,就是釋放每個(gè)節(jié)點(diǎn)的鎖。
當(dāng)然Redlock解決了分布式鎖的基本問題,別忘了它也有《倆個(gè)房客的故事》中的問題,解決方案和單機(jī)版的redis基本一樣。我們可以基于redis-client原生api來實(shí)現(xiàn)Redlock算法,也可以用一些框架,比如Redisson。
Redisson實(shí)現(xiàn)Redlock。語(yǔ)法就比較簡(jiǎn)單
????????RLock?rLock1?=?redissonRed1.getLock(lockKey);
????????RLock?rLock2?=?redissonRed2.getLock(lockKey);
????????RLock?rLock3?=?redissonRed2.getLock(lockKey);
????????RedissonRedLock?rLock?=?new?RedissonRedLock(rLock1,rLock2,rLock3);
????????rLock.lock();
????????try?{
??????//搞事情....
????????}?finally?{
?????????rLock.unlock();
????????}
總結(jié)
我們介紹了redis鎖的一些基本要求,和常見問題,以及解決方案。當(dāng)然還有其他的問題,望大家一起討論。至于Redlock的實(shí)現(xiàn),建議用框架,可以少care一些細(xì)節(jié)。最后揭秘《倆個(gè)房客的故事》純屬本人虛構(gòu),如有雷同,天理不容。
