探索Redis設(shè)計(jì)與實(shí)現(xiàn)15:Redis分布式鎖進(jìn)化史

本文轉(zhuǎn)自互聯(lián)網(wǎng)

本系列文章將整理到我在GitHub上的《Java面試指南》倉(cāng)庫(kù)函筋,更多精彩內(nèi)容請(qǐng)到我的倉(cāng)庫(kù)里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點(diǎn)下Star哈

文章首發(fā)于我的個(gè)人博客:

www.how2playlife.com

本文是微信公眾號(hào)【Java技術(shù)江湖】的《探索Redis設(shè)計(jì)與實(shí)現(xiàn)》其中一篇,本文部分內(nèi)容來(lái)源于網(wǎng)絡(luò)祷杈,為了把本文主題講得清晰透徹达罗,也整合了很多我認(rèn)為不錯(cuò)的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán)燕酷,請(qǐng)聯(lián)系作者。

該系列博文會(huì)告訴你如何從入門到進(jìn)階映企,Redis基本的使用方法悟狱,Redis的基本數(shù)據(jù)結(jié)構(gòu),以及一些進(jìn)階的使用方法堰氓,同時(shí)也需要進(jìn)一步了解Redis的底層數(shù)據(jù)結(jié)構(gòu)挤渐,再接著,還會(huì)帶來(lái)Redis主從復(fù)制双絮、集群浴麻、分布式鎖等方面的相關(guān)內(nèi)容,以及作為緩存的一些使用方法和注意事項(xiàng)囤攀,以便讓你更完整地了解整個(gè)Redis相關(guān)的技術(shù)體系软免,形成自己的知識(shí)框架。

如果對(duì)本系列文章有什么建議焚挠,或者是有什么疑問(wèn)的話膏萧,也可以關(guān)注公眾號(hào)【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。

Redis分布式鎖進(jìn)化史

近兩年來(lái)微服務(wù)變得越來(lái)越熱門榛泛,越來(lái)越多的應(yīng)用部署在分布式環(huán)境中蝌蹂,在分布式環(huán)境中,數(shù)據(jù)一致性是一直以來(lái)需要關(guān)注并且去解決的問(wèn)題曹锨,分布式鎖也就成為了一種廣泛使用的技術(shù)孤个,常用的分布式實(shí)現(xiàn)方式為Redis,Zookeeper沛简,其中基于Redis的分布式鎖的使用更加廣泛齐鲤。

但是在工作和網(wǎng)絡(luò)上看到過(guò)各個(gè)版本的Redis分布式鎖實(shí)現(xiàn),每種實(shí)現(xiàn)都有一些不嚴(yán)謹(jǐn)?shù)牡胤浇烽梗踔劣锌赡苁清e(cuò)誤的實(shí)現(xiàn)给郊,包括在代碼中,如果不能正確的使用分布式鎖捧灰,可能造成嚴(yán)重的生產(chǎn)環(huán)境故障丑罪,本文主要對(duì)目前遇到的各種分布式鎖以及其缺陷做了一個(gè)整理,并對(duì)如何選擇合適的Redis分布式鎖給出建議凤壁。

各個(gè)版本的Redis分布式鎖

  • V1.0
tryLock(){  
    SETNX Key 1
    EXPIRE Key Seconds
}
release(){  
  DELETE Key
}

這個(gè)版本應(yīng)該是最簡(jiǎn)單的版本吩屹,也是出現(xiàn)頻率很高的一個(gè)版本,首先給鎖加一個(gè)過(guò)期時(shí)間操作是為了避免應(yīng)用在服務(wù)重啟或者異常導(dǎo)致鎖無(wú)法釋放后拧抖,不會(huì)出現(xiàn)鎖一直無(wú)法被釋放的情況煤搜。

這個(gè)方案的一個(gè)問(wèn)題在于每次提交一個(gè)Redis請(qǐng)求,如果執(zhí)行完第一條命令后應(yīng)用異尺笙或者重啟擦盾,鎖將無(wú)法過(guò)期,一種改善方案就是使用Lua腳本(包含SETNX和EXPIRE兩條命令)淌哟,但是如果Redis僅執(zhí)行了一條命令后crash或者發(fā)生主從切換迹卢,依然會(huì)出現(xiàn)鎖沒(méi)有過(guò)期時(shí)間,最終導(dǎo)致無(wú)法釋放徒仓。

另外一個(gè)問(wèn)題在于腐碱,很多同學(xué)在釋放分布式鎖的過(guò)程中,無(wú)論鎖是否獲取成功掉弛,都在finally中釋放鎖症见,這樣是一個(gè)鎖的錯(cuò)誤使用,這個(gè)問(wèn)題將在后續(xù)的V3.0版本中解決殃饿。

針對(duì)鎖無(wú)法釋放問(wèn)題的一個(gè)解決方案基于GETSET命令來(lái)實(shí)現(xiàn)

tryLock(){  
    NewExpireTime=CurrentTimestamp+ExpireSeconds
    if(SETNX Key NewExpireTime Seconds){
         oldExpireTime = GET(Key)
          if( oldExpireTime < CurrentTimestamp){
              NewExpireTime=CurrentTimestamp+ExpireSeconds
              CurrentExpireTime=GETSET(Key,NewExpireTime)
              if(CurrentExpireTime == oldExpireTime){
                return 1;
              }else{
                return 0;
              }
          }
    }
}
release(){  
        DELETE key
    }

思路:

  1. SETNX(Key,ExpireTime)獲取鎖

  2. 如果獲取鎖失敗谋作,通過(guò)GET(Key)返回的時(shí)間戳檢查鎖是否已經(jīng)過(guò)期

  3. GETSET(Key,ExpireTime)修改Value為NewExpireTime

  4. 檢查GETSET返回的舊值,如果等于GET返回的值乎芳,則認(rèn)為獲取鎖成功

    注意:這個(gè)版本去掉了EXPIRE命令遵蚜,改為通過(guò)Value時(shí)間戳值來(lái)判斷過(guò)期

問(wèn)題:

  1. 在鎖競(jìng)爭(zhēng)較高的情況下帖池,會(huì)出現(xiàn)Value不斷被覆蓋,但是沒(méi)有一個(gè)Client獲取到鎖  
  
  2. 在獲取鎖的過(guò)程中不斷的修改原有鎖的數(shù)據(jù)吭净,設(shè)想一種場(chǎng)景C1碘裕,C2競(jìng)爭(zhēng)鎖,C1獲取到了鎖攒钳,C2鎖執(zhí)行了GETSET操作修改了C1鎖的過(guò)期時(shí)間,如果C1沒(méi)有正確釋放鎖雷滋,鎖的過(guò)期時(shí)間被延長(zhǎng)不撑,其它Client需要等待更久的時(shí)間
tryLock(){      SETNX Key 1 Seconds}release(){    DELETE Key}

Redis 2.6.12版本后SETNX增加過(guò)期時(shí)間參數(shù),這樣就解決了兩條命令無(wú)法保證原子性的問(wèn)題晤斩。但是設(shè)想下面一個(gè)場(chǎng)景:

1. C1成功獲取到了鎖焕檬,之后C1因?yàn)镚C進(jìn)入等待或者未知原因?qū)е氯蝿?wù)執(zhí)行過(guò)長(zhǎng),最后在鎖失效前C1沒(méi)有主動(dòng)釋放鎖

2.
C2在C1的鎖超時(shí)后獲取到鎖澳泵,并且開(kāi)始執(zhí)行实愚,這個(gè)時(shí)候C1和C2都同時(shí)在執(zhí)行,會(huì)因重復(fù)執(zhí)行造成數(shù)據(jù)不一致等未知情況

3. C1如果先執(zhí)行完畢兔辅,則會(huì)釋放C2的鎖腊敲,此時(shí)可能導(dǎo)致另外一個(gè)C3進(jìn)程獲取到了鎖

大致的流程圖

存在問(wèn)題:

1\. 由于C1的停頓導(dǎo)致C1 和C2同都獲得了鎖并且同時(shí)在執(zhí)行,在業(yè)務(wù)實(shí)現(xiàn)間接要求必須保證冪等性

2\. C1釋放了不屬于C1的鎖
  • V3.0
tryLock(){  
    SETNX Key UnixTimestamp Seconds
}
release(){  
    EVAL(
      //LuaScript
      if redis.call("get",KEYS[1]) == ARGV[1] then
          return redis.call("del",KEYS[1])
      else
          return 0
      end
    )
}

這個(gè)方案通過(guò)指定Value為時(shí)間戳维苔,并在釋放鎖的時(shí)候檢查鎖的Value是否為獲取鎖的Value碰辅,避免了V2.0版本中提到的C1釋放了C2持有的鎖的問(wèn)題;另外在釋放鎖的時(shí)候因?yàn)樯婕暗蕉鄠€(gè)Redis操作介时,并且考慮到Check And Set 模型的并發(fā)問(wèn)題没宾,所以使用Lua腳本來(lái)避免并發(fā)問(wèn)題。

存在問(wèn)題:

如果在并發(fā)極高的場(chǎng)景下沸柔,比如搶紅包場(chǎng)景循衰,可能存在UnixTimestamp重復(fù)問(wèn)題,另外由于不能保證分布式環(huán)境下的物理時(shí)鐘一致性褐澎,也可能存在UnixTimestamp重復(fù)問(wèn)題会钝,只不過(guò)極少情況下會(huì)遇到。

  • V3.1
tryLock(){  
    SET Key UniqId Seconds
}
release(){  
    EVAL(
      //LuaScript
      if redis.call("get",KEYS[1]) == ARGV[1] then
          return redis.call("del",KEYS[1])
      else
          return 0
      end
    )
}

Redis 2.6.12后SET同樣提供了一個(gè)NX參數(shù)工三,等同于SETNX命令顽素,官方文檔上提醒后面的版本有可能去掉SETNX, SETEX, PSETEX,并用SET命令代替,另外一個(gè)優(yōu)化是使用一個(gè)自增的唯一UniqId代替時(shí)間戳來(lái)規(guī)避V3.0提到的時(shí)鐘問(wèn)題徒蟆。

這個(gè)方案是目前最優(yōu)的分布式鎖方案胁出,但是如果在Redis集群環(huán)境下依然存在問(wèn)題:

由于Redis集群數(shù)據(jù)同步為異步,假設(shè)在Master節(jié)點(diǎn)獲取到鎖后未完成數(shù)據(jù)同步情況下Master節(jié)點(diǎn)crash段审,此時(shí)在新的Master節(jié)點(diǎn)依然可以獲取鎖全蝶,所以多個(gè)Client同時(shí)獲取到了鎖

分布式Redis鎖:Redlock

V3.1的版本僅在單實(shí)例的場(chǎng)景下是安全的,針對(duì)如何實(shí)現(xiàn)分布式Redis的鎖,國(guó)外的分布式專家有過(guò)激烈的討論抑淫, antirez提出了分布式鎖算法Redlock绷落,在distlock話題下可以看到對(duì)Redlock的詳細(xì)說(shuō)明,下面是Redlock算法的一個(gè)中文說(shuō)明(引用)

假設(shè)有N個(gè)獨(dú)立的Redis節(jié)點(diǎn)

  1. 獲取當(dāng)前時(shí)間(毫秒數(shù))始苇。

  2. 按順序依次向N個(gè)Redis節(jié)點(diǎn)執(zhí)行獲取鎖的操作砌烁。這個(gè)獲取操作跟前面基于單Redis節(jié)點(diǎn)的獲取鎖的過(guò)程相同,包含隨機(jī)字符串my_random_value催式,也包含過(guò)期時(shí)間(比如PX 30000函喉,即鎖的有效時(shí)間)。為了保證在某個(gè)Redis節(jié)點(diǎn)不可用的時(shí)候算法能夠繼續(xù)運(yùn)行荣月,這個(gè)獲取鎖的操作還有一個(gè)超時(shí)時(shí)間(time out)管呵,它要遠(yuǎn)小于鎖的有效時(shí)間(幾十毫秒量級(jí))〔刚客戶端在向某個(gè)Redis節(jié)點(diǎn)獲取鎖失敗以后捐下,應(yīng)該立即嘗試下一個(gè)Redis節(jié)點(diǎn)。這里的失敗萌业,應(yīng)該包含任何類型的失敗坷襟,比如該Redis節(jié)點(diǎn)不可用,或者該Redis節(jié)點(diǎn)上的鎖已經(jīng)被其它客戶端持有(注:Redlock原文中這里只提到了Redis節(jié)點(diǎn)不可用的情況生年,但也應(yīng)該包含其它的失敗情況)啤握。

  3. 計(jì)算整個(gè)獲取鎖的過(guò)程總共消耗了多長(zhǎng)時(shí)間,計(jì)算方法是用當(dāng)前時(shí)間減去第1步記錄的時(shí)間晶框。如果客戶端從大多數(shù)Redis節(jié)點(diǎn)(>= N/2+1)成功獲取到了鎖排抬,并且獲取鎖總共消耗的時(shí)間沒(méi)有超過(guò)鎖的有效時(shí)間(lock validity time),那么這時(shí)客戶端才認(rèn)為最終獲取鎖成功授段;否則蹲蒲,認(rèn)為最終獲取鎖失敗。

  4. 如果最終獲取鎖成功了侵贵,那么這個(gè)鎖的有效時(shí)間應(yīng)該重新計(jì)算届搁,它等于最初的鎖的有效時(shí)間減去第3步計(jì)算出來(lái)的獲取鎖消耗的時(shí)間。

  5. 如果最終獲取鎖失敗了(可能由于獲取到鎖的Redis節(jié)點(diǎn)個(gè)數(shù)少于N/2+1窍育,或者整個(gè)獲取鎖的過(guò)程消耗的時(shí)間超過(guò)了鎖的最初有效時(shí)間)卡睦,那么客戶端應(yīng)該立即向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作(即前面介紹的Redis Lua腳本)。

  6. 釋放鎖:對(duì)所有的Redis節(jié)點(diǎn)發(fā)起釋放鎖操作

然而Martin Kleppmann針對(duì)這個(gè)算法提出了質(zhì)疑漱抓,提出應(yīng)該基于fencing token機(jī)制(每次對(duì)資源進(jìn)行操作都需要進(jìn)行token驗(yàn)證)

  1. Redlock在系統(tǒng)模型上尤其是在分布式時(shí)鐘一致性問(wèn)題上提出了假設(shè)表锻,實(shí)際場(chǎng)景下存在時(shí)鐘不一致和時(shí)鐘跳躍問(wèn)題,而Redlock恰恰是基于timing的分布式鎖  2. 另外Redlock由于是基于自動(dòng)過(guò)期機(jī)制乞娄,依然沒(méi)有解決長(zhǎng)時(shí)間的gc pause等問(wèn)題帶來(lái)的鎖自動(dòng)失效瞬逊,從而帶來(lái)的安全性問(wèn)題显歧。

接著antirez又回復(fù)了Martin Kleppmann的質(zhì)疑,給出了過(guò)期機(jī)制的合理性确镊,以及實(shí)際場(chǎng)景中如果出現(xiàn)停頓問(wèn)題導(dǎo)致多個(gè)Client同時(shí)訪問(wèn)資源的情況下如何處理士骤。

針對(duì)Redlock的問(wèn)題,基于Redis的分布式鎖到底安全嗎給出了詳細(xì)的中文說(shuō)明蕾域,并對(duì)Redlock算法存在的問(wèn)題提出了分析拷肌。

總結(jié)

不論是基于SETNX版本的Redis單實(shí)例分布式鎖,還是Redlock分布式鎖旨巷,都是為了保證下特性

  1\. 安全性:在同一時(shí)間不允許多個(gè)Client同時(shí)持有鎖  2\. 活性    死鎖:鎖最終應(yīng)該能夠被釋放巨缘,即使Client端crash或者出現(xiàn)網(wǎng)絡(luò)分區(qū)(通常基于超時(shí)機(jī)制)    容錯(cuò)性:只要超過(guò)半數(shù)Redis節(jié)點(diǎn)可用契沫,鎖都能被正確獲取和釋放

所以在開(kāi)發(fā)或者使用分布式鎖的過(guò)程中要保證安全性和活性,避免出現(xiàn)不可預(yù)測(cè)的結(jié)果昔汉。

另外每個(gè)版本的分布式鎖都存在一些問(wèn)題懈万,在鎖的使用上要針對(duì)鎖的實(shí)用場(chǎng)景選擇合適的鎖,通常情況下鎖的使用場(chǎng)景包括:

Efficiency(效率):只需要一個(gè)Client來(lái)完成操作靶病,不需要重復(fù)執(zhí)行会通,這是一個(gè)對(duì)寬松的分布式鎖,只需要保證鎖的活性即可娄周;

Correctness(正確性):多個(gè)Client保證嚴(yán)格的互斥性涕侈,不允許出現(xiàn)同時(shí)持有鎖或者對(duì)同時(shí)操作同一資源,這種場(chǎng)景下需要在鎖的選擇和使用上更加嚴(yán)格煤辨,同時(shí)在業(yè)務(wù)代碼上盡量做到冪等

在Redis分布式鎖的實(shí)現(xiàn)上還有很多問(wèn)題等待解決裳涛,我們需要認(rèn)識(shí)到這些問(wèn)題并清楚如何正確實(shí)現(xiàn)一個(gè)Redis 分布式鎖,然后在工作中合理的選擇和正確的使用分布式鎖众辨。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末端三,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鹃彻,更是在濱河造成了極大的恐慌郊闯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛛株,死亡現(xiàn)場(chǎng)離奇詭異团赁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)谨履,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門欢摄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人笋粟,你說(shuō)我怎么就攤上這事剧浸∏乱” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵唆香,是天一觀的道長(zhǎng)嫌变。 經(jīng)常有香客問(wèn)我,道長(zhǎng)躬它,這世上最難降的妖魔是什么腾啥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮冯吓,結(jié)果婚禮上倘待,老公的妹妹穿的比我還像新娘。我一直安慰自己组贺,他們只是感情好凸舵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著失尖,像睡著了一般啊奄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掀潮,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天菇夸,我揣著相機(jī)與錄音,去河邊找鬼仪吧。 笑死庄新,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的薯鼠。 我是一名探鬼主播择诈,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼出皇!你這毒婦竟也來(lái)了吭从?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤恶迈,失蹤者是張志新(化名)和其女友劉穎涩金,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體暇仲,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡步做,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了奈附。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片全度。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖斥滤,靈堂內(nèi)的尸體忽然破棺而出将鸵,到底是詐尸還是另有隱情勉盅,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布顶掉,位于F島的核電站草娜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏痒筒。R本人自食惡果不足惜宰闰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望簿透。 院中可真熱鬧移袍,春花似錦、人聲如沸老充。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)啡浊。三九已至觅够,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虫啥,已是汗流浹背蔚约。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工奄妨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涂籽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓砸抛,卻偏偏與公主長(zhǎng)得像评雌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子直焙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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