Redis分布式鎖

為什么要用Redis

分布式環(huán)境考慮加鎖,可以想到如下方法

  1. 數(shù)據(jù)庫(kù)字段
  2. 基于Zookeeper管理機(jī)器
  3. 基于緩存,可以適用Redis

基于數(shù)據(jù)庫(kù)的方式個(gè)人感覺意義不大膜蛔,因?yàn)榇蠖鄶?shù)鎖說(shuō)需要保存的值非常少璧微,為此建庫(kù)建表意義不大仰猖,而且查詢速度還比較慢照皆。性能不佳

而基于Zookeeper,可以對(duì)于每個(gè)客戶端對(duì)某個(gè)方法加鎖時(shí)典阵,在zookeeper上的與該方法對(duì)應(yīng)的指定節(jié)點(diǎn)的目錄下,生成一個(gè)唯一的瞬時(shí)有序節(jié)點(diǎn)镊逝。 判斷是否獲取鎖的方式很簡(jiǎn)單壮啊,只需要判斷有序節(jié)點(diǎn)中序號(hào)最小的一個(gè)。 當(dāng)釋放鎖的時(shí)候撑蒜,只需將這個(gè)瞬時(shí)節(jié)點(diǎn)刪除即可他巨。同時(shí)充坑,其可以避免服務(wù)宕機(jī)導(dǎo)致的鎖無(wú)法釋放减江,而產(chǎn)生的死鎖問題染突。 問題是較為麻煩,而且效率沒有使用緩存高辈灼。

如果基于緩存呢?首先性能比較好讀取很快份企,而且像Redis都是已有部署好的集群可以直接使用。

實(shí)現(xiàn)

主要是使用SETNX()方法 全稱就是SET IF NOT EXIST

  • 返回1 說(shuō)明在Redis中set了key巡莹,獲得鎖
  • 返回0 說(shuō)明該key已經(jīng)被set司志,不能獲得鎖

看似很美好 直接一句話就可以實(shí)現(xiàn)了 但是其實(shí)存在死鎖的問題

死鎖問題

無(wú)論這個(gè)鎖是干什么用的 都要在使用后放開鎖 否則會(huì)讓其他競(jìng)爭(zhēng)者永久等待
對(duì)于這個(gè)問題一般都是考慮使用設(shè)置超時(shí)來(lái)實(shí)現(xiàn)的

錯(cuò)誤的處理

先來(lái)看幾個(gè)我親自犯過(guò)的錯(cuò)誤 一定認(rèn)真看一下 可能你第一次寫也是這樣考慮的 如果實(shí)在等不急可以先去偷看一下正確答案。

錯(cuò)誤A

Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
    // 若在這里程序突然崩潰降宅,則無(wú)法設(shè)置過(guò)期時(shí)間骂远,將發(fā)生死鎖
    jedis.expire(lockKey, expireTime);
}

這是是第一次寫的時(shí)候出現(xiàn)的問題 先通過(guò)一條命令嘗試加鎖再設(shè)置過(guò)期時(shí)間,但是這里有個(gè)坑腰根,就是如果在嘗試加鎖完成以后程序崩了激才。GG這個(gè)鎖這輩子也釋放不了了,標(biāo)準(zhǔn)的死鎖额嘿。

錯(cuò)誤B

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
 
    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);
 
    // 如果當(dāng)前鎖不存在瘸恼,返回加鎖成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }
 
    // 如果鎖存在,獲取鎖的過(guò)期時(shí)間
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 鎖已過(guò)期册养,獲取上一個(gè)鎖的過(guò)期時(shí)間东帅,并設(shè)置現(xiàn)在鎖的過(guò)期時(shí)間
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考慮多線程并發(fā)的情況,只有一個(gè)線程的設(shè)置值和當(dāng)前值相同球拦,它才有權(quán)利加鎖
            return true;
        }
    }
 
    // 其他情況靠闭,一律返回加鎖失敗
    return false;
 
}

這里看似很完美,通過(guò)對(duì)Value設(shè)置時(shí)間戳的方式防止之前的線程掛掉的情況坎炼,但是我們?cè)倏匆幌箩尫沛i的方法

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

設(shè)想一個(gè)情況点弯,線程A加鎖并設(shè)置過(guò)期時(shí)間扇调。突然線程A掛了這是線程B苦苦等到了過(guò)期時(shí)間成功拿到了鎖。正準(zhǔn)備爽一下的時(shí)候抢肛,突然A滿血復(fù)活了狼钮,可能會(huì)“正常”的釋放鎖捡絮。B就不能忍了熬芜,我等你這么長(zhǎng)時(shí)間好不容易拿到了鎖,你回來(lái)直接給我釋放了福稳。

A加鎖 - A死亡 - 超時(shí) - B加鎖 - A復(fù)活 - A釋放鎖(這時(shí)B還在執(zhí)行)

說(shuō)了這么多涎拉,都感覺Redis是不是不適合做分布式鎖啊!那我們來(lái)看一下正確答案鼓拧。

正確答案

這里我也是學(xué)習(xí)了別人的代碼半火,需要使用Lua腳本。

 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)) {
        return true;
}
return false;

第二行代碼季俩,我們將Lua代碼傳到j(luò)edis.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)橐_保上述操作是原子性的。

因?yàn)椋篹val命令執(zhí)行Lua代碼的時(shí)候质涛,Lua代碼將被當(dāng)成一個(gè)命令去執(zhí)行稠歉,并且直到eval命令執(zhí)行完成,Redis才會(huì)執(zhí)行其他命令汇陆。保證了其原子性怒炸。

最后

其實(shí)Redis本身實(shí)現(xiàn)的分布式鎖的確存在各種問題。有人認(rèn)為它并不安全
但是對(duì)于Redis是多機(jī)部署的毡代,那么可以嘗試使用Redisson實(shí)現(xiàn)分布式鎖阅羹,這是Redis官方提供的Java組件,這里有一篇網(wǎng)易技術(shù)的博客可以看一下.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市教寂,隨后出現(xiàn)的幾起案子捏鱼,更是在濱河造成了極大的恐慌,老刑警劉巖酪耕,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件导梆,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡迂烁,警方通過(guò)查閱死者的電腦和手機(jī)看尼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)盟步,“玉大人藏斩,你說(shuō)我怎么就攤上這事∪磁蹋” “怎么了狰域?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵媳拴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我兆览,道長(zhǎng)屈溉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任拓颓,我火速辦了婚禮语婴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驶睦。我一直安慰自己,他們只是感情好匿醒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布场航。 她就那樣靜靜地躺著,像睡著了一般廉羔。 火紅的嫁衣襯著肌膚如雪溉痢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天憋他,我揣著相機(jī)與錄音孩饼,去河邊找鬼。 笑死竹挡,一個(gè)胖子當(dāng)著我的面吹牛镀娶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播揪罕,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼梯码,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了好啰?” 一聲冷哼從身側(cè)響起轩娶,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎框往,沒想到半個(gè)月后鳄抒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡椰弊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年许溅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片男应。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闹司,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沐飘,到底是詐尸還是另有隱情游桩,我是刑警寧澤牲迫,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站借卧,受9級(jí)特大地震影響盹憎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铐刘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一陪每、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧镰吵,春花似錦檩禾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至勺馆,卻和暖如春戏售,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背草穆。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工灌灾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悲柱。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓锋喜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親诗祸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子跑芳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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