Redis分布式鎖---完美實(shí)現(xiàn)

前言

分布式鎖一般有三種實(shí)現(xiàn)方式:1. 數(shù)據(jù)庫(kù)樂(lè)觀鎖渗磅;2. 基于Redis的分布式鎖;3. 基于ZooKeeper的分布式鎖冀痕。本篇博客將介紹第二種方式币狠,基于Redis實(shí)現(xiàn)分布式鎖听绳。雖然網(wǎng)上已經(jīng)有各種介紹Redis分布式鎖實(shí)現(xiàn)的博客,然而他們的實(shí)現(xiàn)卻有著各種各樣的問(wèn)題,為了避免誤人子弟,本篇博客將詳細(xì)介紹如何正確地實(shí)現(xiàn)Redis分布式鎖帜羊。

可靠性

首先咒程,為了確保分布式鎖可用鸠天,我們至少要確保鎖的實(shí)現(xiàn)同時(shí)滿足以下四個(gè)條件:

互斥性。在任意時(shí)刻帐姻,只有一個(gè)客戶端能持有鎖稠集。

不會(huì)發(fā)生死鎖。即使有一個(gè)客戶端在持有鎖的期間崩潰而沒(méi)有主動(dòng)解鎖饥瓷,也能保證后續(xù)其他客戶端能加鎖剥纷。

具有容錯(cuò)性。只要大部分的Redis節(jié)點(diǎn)正常運(yùn)行呢铆,客戶端就可以加鎖和解鎖晦鞋。

解鈴還須系鈴人。加鎖和解鎖必須是同一個(gè)客戶端,客戶端自己不能把別人加的鎖給解了悠垛。

代碼實(shí)現(xiàn)

組件依賴

首先我們要通過(guò)Maven引入?Jedis?開(kāi)源組件线定,在?pom.xml?文件加入下面的代碼:

redis.clientsjedis2.9.0

加鎖代碼

正確姿勢(shì)

Talk is cheap, show me the code。先展示代碼确买,再帶大家慢慢解釋為什么這樣實(shí)現(xiàn):

publicclassRedisTool{privatestaticfinalString LOCK_SUCCESS ="OK";privatestaticfinalString SET_IF_NOT_EXIST ="NX";privatestaticfinalString SET_WITH_EXPIRE_TIME ="PX";/**? ? * 嘗試獲取分布式鎖? ? *@paramjedis Redis客戶端? ? *@paramlockKey 鎖? ? *@paramrequestId 請(qǐng)求標(biāo)識(shí)? ? *@paramexpireTime 超期時(shí)間? ? *@return是否獲取成功? ? */publicstaticbooleantryGetDistributedLock(Jedis jedis, String lockKey, String requestId,intexpireTime){? ? ? ? String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if(LOCK_SUCCESS.equals(result)) {returntrue;? ? ? ? }returnfalse;? ? }}

可以看到斤讥,我們加鎖就一行代碼:?jedis.set(String key, String value, String nxxx, String expx, int time)?,這個(gè)set()方法一共有五個(gè)形參:

第一個(gè)為key湾趾,我們使用key來(lái)當(dāng)鎖芭商,因?yàn)閗ey是唯一的。

第二個(gè)為value搀缠,我們傳的是requestId铛楣,很多童鞋可能不明白,有key作為鎖不就夠了嗎胡嘿,為什么還要用到value蛉艾?原因就是我們?cè)谏厦嬷v到可靠性時(shí),分布式鎖要滿足第四個(gè)條件解鈴還須系鈴人衷敌,通過(guò)給value賦值為requestId勿侯,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候就可以有依據(jù)缴罗。requestId可以使用?UUID.randomUUID().toString()?方法生成助琐。

第三個(gè)為nxxx,這個(gè)參數(shù)我們填的是NX面氓,意思是SET IF NOT EXIST兵钮,即當(dāng)key不存在時(shí),我們進(jìn)行set操作舌界;若key已經(jīng)存在掘譬,則不做任何操作;

第四個(gè)為expx呻拌,這個(gè)參數(shù)我們傳的是PX葱轩,意思是我們要給這個(gè)key加一個(gè)過(guò)期的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定藐握。

第五個(gè)為time靴拱,與第四個(gè)參數(shù)相呼應(yīng),代表key的過(guò)期時(shí)間猾普。

總的來(lái)說(shuō)袜炕,執(zhí)行上面的set()方法就只會(huì)導(dǎo)致兩種結(jié)果:1. 當(dāng)前沒(méi)有鎖(key不存在),那么就進(jìn)行加鎖操作初家,并對(duì)鎖設(shè)置個(gè)有效期偎窘,同時(shí)value表示加鎖的客戶端乌助。2. 已有鎖存在,不做任何操作陌知。

心細(xì)的童鞋就會(huì)發(fā)現(xiàn)了眷茁,我們的加鎖代碼滿足我們可靠性里描述的三個(gè)條件。首先纵诞,set()加入了NX參數(shù)上祈,可以保證如果已有key存在,則函數(shù)不會(huì)調(diào)用成功浙芙,也就是只有一個(gè)客戶端能持有鎖登刺,滿足互斥性。其次嗡呼,由于我們對(duì)鎖設(shè)置了過(guò)期時(shí)間纸俭,即使鎖的持有者后續(xù)發(fā)生崩潰而沒(méi)有解鎖,鎖也會(huì)因?yàn)榈搅诉^(guò)期時(shí)間而自動(dòng)解鎖(即key被刪除)南窗,不會(huì)發(fā)生死鎖揍很。最后,因?yàn)槲覀儗alue賦值為requestId万伤,代表加鎖的客戶端請(qǐng)求標(biāo)識(shí)窒悔,那么在客戶端在解鎖的時(shí)候就可以進(jìn)行校驗(yàn)是否是同一個(gè)客戶端。由于我們只考慮Redis單機(jī)部署的場(chǎng)景敌买,所以容錯(cuò)性我們暫不考慮简珠。

錯(cuò)誤示例1

比較常見(jiàn)的錯(cuò)誤示例就是使用?jedis.setnx()?和?jedis.expire()?組合實(shí)現(xiàn)加鎖,代碼如下:

publicstaticvoidwrongGetLock1(Jedis jedis,StringlockKey,StringrequestId,intexpireTime) {? ? Long result = jedis.setnx(lockKey, requestId);if(result ==1) {// 若在這里程序突然崩潰虹钮,則無(wú)法設(shè)置過(guò)期時(shí)間聋庵,將發(fā)生死鎖jedis.expire(lockKey, expireTime);? ? }}

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是給鎖加一個(gè)過(guò)期時(shí)間芙粱。乍一看好像和前面的set()方法結(jié)果一樣祭玉,然而由于這是兩條Redis命令,不具有原子性春畔,如果程序在執(zhí)行完setnx()之后突然崩潰脱货,導(dǎo)致鎖沒(méi)有設(shè)置過(guò)期時(shí)間。那么將會(huì)發(fā)生死鎖拐迁。網(wǎng)上之所以有人這樣實(shí)現(xiàn)蹭劈,是因?yàn)榈桶姹镜膉edis并不支持多參數(shù)的set()方法疗绣。

錯(cuò)誤示例2

publicstaticboolean wrongGetLock2(Jedis jedis,StringlockKey,intexpireTime) {? ? long expires = System.currentTimeMillis() + expireTime;StringexpiresStr =String.valueOf(expires);// 如果當(dāng)前鎖不存在线召,返回加鎖成功if(jedis.setnx(lockKey, expiresStr) ==1) {returntrue;? ? }// 如果鎖存在,獲取鎖的過(guò)期時(shí)間StringcurrentValueStr = jedis.get(lockKey);if(currentValueStr !=null&& Long.parseLong(currentValueStr) < System.currentTimeMillis()) {// 鎖已過(guò)期多矮,獲取上一個(gè)鎖的過(guò)期時(shí)間缓淹,并設(shè)置現(xiàn)在鎖的過(guò)期時(shí)間StringoldValueStr = jedis.getSet(lockKey, expiresStr);if(oldValueStr !=null&& oldValueStr.equals(currentValueStr)) {// 考慮多線程并發(fā)的情況哈打,只有一個(gè)線程的設(shè)置值和當(dāng)前值相同,它才有權(quán)利加鎖returntrue;? ? ? ? }? ? }// 其他情況讯壶,一律返回加鎖失敗returnfalse;}

這一種錯(cuò)誤示例就比較難以發(fā)現(xiàn)問(wèn)題料仗,而且實(shí)現(xiàn)也比較復(fù)雜。實(shí)現(xiàn)思路:使用?jedis.setnx()命令實(shí)現(xiàn)加鎖伏蚊,其中key是鎖立轧,value是鎖的過(guò)期時(shí)間。執(zhí)行過(guò)程:1. 通過(guò)setnx()方法嘗試加鎖躏吊,如果當(dāng)前鎖不存在氛改,返回加鎖成功。2. 如果鎖已經(jīng)存在則獲取鎖的過(guò)期時(shí)間比伏,和當(dāng)前時(shí)間比較胜卤,如果鎖已經(jīng)過(guò)期,則設(shè)置新的過(guò)期時(shí)間赁项,返回加鎖成功葛躏。代碼如下:

那么這段代碼問(wèn)題在哪里?1. 由于是客戶端自己生成過(guò)期時(shí)間悠菜,所以需要強(qiáng)制要求分布式下每個(gè)客戶端的時(shí)間必須同步舰攒。 2. 當(dāng)鎖過(guò)期的時(shí)候,如果多個(gè)客戶端同時(shí)執(zhí)行?jedis.getSet()?方法悔醋,那么雖然最終只有一個(gè)客戶端可以加鎖芒率,但是這個(gè)客戶端的鎖的過(guò)期時(shí)間可能被其他客戶端覆蓋。3. 鎖不具備擁有者標(biāo)識(shí)篙顺,即任何客戶端都可以解鎖偶芍。

解鎖代碼

正確姿勢(shì)

還是先展示代碼,再帶大家慢慢解釋為什么這樣實(shí)現(xiàn):

publicclassRedisTool{privatestaticfinalLong RELEASE_SUCCESS =1L;/**? ? * 釋放分布式鎖? ? *@paramjedis Redis客戶端? ? *@paramlockKey 鎖? ? *@paramrequestId 請(qǐng)求標(biāo)識(shí)? ? *@return是否釋放成功? ? */publicstaticboolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {? ? ? ? String script ="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";? ? ? ? Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));if(RELEASE_SUCCESS.equals(result)) {returntrue;? ? ? ? }returnfalse;? ? }}

可以看到德玫,我們解鎖只需要兩行代碼就搞定了匪蟀!第一行代碼,我們寫(xiě)了一個(gè)簡(jiǎn)單的Lua腳本代碼宰僧,上一次見(jiàn)到這個(gè)編程語(yǔ)言還是在《黑客與畫(huà)家》里材彪,沒(méi)想到這次居然用上了。第二行代碼琴儿,我們將Lua代碼傳到?jedis.eval()?方法里段化,并使參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId造成。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行显熏。

那么這段Lua代碼的功能是什么呢?其實(shí)很簡(jiǎn)單晒屎,首先獲取鎖對(duì)應(yīng)的value值喘蟆,檢查是否與requestId相等缓升,如果相等則刪除鎖(解鎖)。那么為什么要使用Lua語(yǔ)言來(lái)實(shí)現(xiàn)呢蕴轨?因?yàn)橐_保上述操作是原子性的港谊。關(guān)于非原子性會(huì)帶來(lái)什么問(wèn)題,可以閱讀?【解鎖代碼-錯(cuò)誤示例2】?橙弱。那么為什么執(zhí)行eval()方法可以確保原子性歧寺,源于Redis的特性,下面是官網(wǎng)對(duì)eval命令的部分解釋:

簡(jiǎn)單來(lái)說(shuō)棘脐,就是在eval命令執(zhí)行Lua代碼的時(shí)候成福,Lua代碼將被當(dāng)成一個(gè)命令去執(zhí)行,并且直到eval命令執(zhí)行完成荆残,Redis才會(huì)執(zhí)行其他命令奴艾。

錯(cuò)誤示例1

最常見(jiàn)的解鎖代碼就是直接使用?jedis.del()?方法刪除鎖,這種不先判斷鎖的擁有者而直接解鎖的方式内斯,會(huì)導(dǎo)致任何客戶端都可以隨時(shí)進(jìn)行解鎖蕴潦,即使這把鎖不是它的。

publicstaticvoidwrongReleaseLock1(Jedis jedis, String lockKey){? ? jedis.del(lockKey);}

錯(cuò)誤示例2

這種解鎖代碼乍一看也是沒(méi)問(wèn)題俘闯,甚至我之前也差點(diǎn)這樣實(shí)現(xiàn)潭苞,與正確姿勢(shì)差不多,唯一區(qū)別的是分成兩條命令去執(zhí)行真朗,代碼如下:

publicstaticvoidwrongReleaseLock2(Jedis jedis, String lockKey, String requestId){// 判斷加鎖與解鎖是不是同一個(gè)客戶端if(requestId.equals(jedis.get(lockKey))) {// 若在此時(shí)此疹,這把鎖突然不是這個(gè)客戶端的,則會(huì)誤解鎖jedis.del(lockKey);? ? }}

如代碼注釋遮婶,問(wèn)題在于如果調(diào)用?jedis.del()?方法的時(shí)候蝗碎,這把鎖已經(jīng)不屬于當(dāng)前客戶端的時(shí)候會(huì)解除他人加的鎖。那么是否真的有這種場(chǎng)景旗扑?答案是肯定的蹦骑,比如客戶端A加鎖,一段時(shí)間之后客戶端A解鎖臀防,在執(zhí)行?jedis.del()?之前眠菇,鎖突然過(guò)期了,此時(shí)客戶端B嘗試加鎖成功袱衷,然后客戶端A再執(zhí)行del()方法捎废,則將客戶端B的鎖給解除了。

總結(jié)

本文主要介紹了如何使用Java代碼正確實(shí)現(xiàn)Redis分布式鎖致燥,對(duì)于加鎖和解鎖也分別給出了兩個(gè)比較經(jīng)典的錯(cuò)誤示例登疗。其實(shí)想要通過(guò)Redis實(shí)現(xiàn)分布式鎖并不難,只要保證能滿足可靠性里的四個(gè)條件篡悟∶仗荆互聯(lián)網(wǎng)雖然給我們帶來(lái)了方便,只要有問(wèn)題就可以google搬葬,然而網(wǎng)上的答案一定是對(duì)的嗎荷腊?其實(shí)不然,所以我們更應(yīng)該時(shí)刻保持著質(zhì)疑精神急凰,多想多驗(yàn)證女仰。

如果你的項(xiàng)目中Redis是多機(jī)部署的,那么可以嘗試使用?Redisson?實(shí)現(xiàn)分布式鎖抡锈。

想要了解更多分布式知識(shí)點(diǎn)的疾忍,可以關(guān)注我一下,我后續(xù)也會(huì)整理更多關(guān)于分布式架構(gòu)這一塊的知識(shí)點(diǎn)分享出來(lái)床三,另外順便給大家推薦一個(gè)交流學(xué)習(xí)群:650385180一罩,里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis撇簿,Netty源碼分析聂渊,高并發(fā)、高性能四瘫、分布式汉嗽、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化這些成為架構(gòu)師必備的知識(shí)體系找蜜。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源饼暑,目前受益良多,以下的課程體系圖也是在群里獲取洗做。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末弓叛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子诚纸,更是在濱河造成了極大的恐慌邪码,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咬清,死亡現(xiàn)場(chǎng)離奇詭異闭专,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)旧烧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)影钉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人掘剪,你說(shuō)我怎么就攤上這事平委。” “怎么了夺谁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵廉赔,是天一觀的道長(zhǎng)肉微。 經(jīng)常有香客問(wèn)我,道長(zhǎng)蜡塌,這世上最難降的妖魔是什么碉纳? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮馏艾,結(jié)果婚禮上劳曹,老公的妹妹穿的比我還像新娘。我一直安慰自己琅摩,他們只是感情好铁孵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著房资,像睡著了一般蜕劝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上轰异,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天熙宇,我揣著相機(jī)與錄音,去河邊找鬼溉浙。 笑死烫止,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的戳稽。 我是一名探鬼主播馆蠕,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惊奇!你這毒婦竟也來(lái)了互躬?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤颂郎,失蹤者是張志新(化名)和其女友劉穎吼渡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體乓序,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寺酪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了替劈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寄雀。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖陨献,靈堂內(nèi)的尸體忽然破棺而出盒犹,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布急膀,位于F島的核電站沮协,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏卓嫂。R本人自食惡果不足惜慷暂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望命黔。 院中可真熱鬧呜呐,春花似錦就斤、人聲如沸悍募。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)坠宴。三九已至,卻和暖如春绷旗,著一層夾襖步出監(jiān)牢的瞬間喜鼓,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工衔肢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庄岖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓角骤,卻偏偏與公主長(zhǎng)得像隅忿,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子邦尊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容