Redis、Zookeeper實現(xiàn)分布式鎖——原理與實踐

Redis與分布式鎖的問題已經(jīng)是老生常談了磺浙,本文嘗試總結(jié)一些Redis洪囤、Zookeeper實現(xiàn)分布式鎖的常用方案,并提供一些比較好的實踐思路(基于Java)撕氧。不足之處瘤缩,歡迎探討。

Redis分布式鎖

單機(jī)Redis下實現(xiàn)分布式鎖

方案1:使用SET命令伦泥。
假如當(dāng)前客戶端需要占有一個user_lock的鎖剥啤,它首次需要生成一個token(一個隨機(jī)字符串,例如uiid)不脯,并使用該token進(jìn)行加鎖府怯。

加鎖命令:

redis> SET user_lock <token> EX 15 NX
OK

EX:該鍵會在指定時間后指定過期,單位為秒防楷,類似參數(shù)還有PX牺丙、EXAT、PXAT域帐。
NX:只有該鍵不存在的時候才會設(shè)置key的值赘被。

所以如果user_lock鍵不存在是整,上面Redis命令會成功創(chuàng)建該Redis鍵肖揣,并設(shè)置該鍵在15秒后過期。
而其他客戶端也使用該命令進(jìn)行加鎖浮入,在這15秒時間內(nèi)龙优,其他客戶端加鎖失敗(NX參數(shù)保證了該Redis鍵存在時命令執(zhí)行失敗)彤断。
所以野舶,當(dāng)前客戶端中鎖定了user_lock,鎖的有效時間為15秒宰衙。

為什么要使用token平道、有效期呢?有以下原因:
(1)鎖的有效期可以保證不會發(fā)生死鎖的情況供炼。通常占有鎖的客戶端操作完成后需要釋放鎖(刪除Redis鍵)一屋,使用鎖有效期后,即使占有鎖的客戶端故障下線袋哼,15秒后鎖也會自動失效冀墨,其他客戶端就可以搶占該鎖,不會出現(xiàn)死鎖的情況涛贯。
(2)token的作用是防止客戶端釋放了不是自己占有的鎖诽嘉。客戶端釋放鎖時需要檢測該鎖當(dāng)前是否為自己所占有弟翘,即鍵user_lock的值是否為自己的token虫腋,如果是才可以刪除該鍵。
這里涉及兩個命令稀余,可以lua腳本保證原子性岔乔。如下面命令:

> EVAL "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 user_lock <token>
(integer) 1

如果不使用token,所以客戶端都使用同一個值作為鍵user_lock的值滚躯,假如客戶端A占有了鎖user_lock雏门,但由于過期時間到了,user_lock鍵被Redis服務(wù)器刪除掸掏,這時客戶端B占有了鎖茁影。而客戶端A操作后,直接使用DEL命令刪除當(dāng)前user_lock鍵丧凤,這樣客戶端A就刪除了非自己占有的鎖募闲。

該方案可參考官方文檔:https://redis.io/commands/set

從上面內(nèi)容可以看到,該方案的分布式鎖并不是安全的愿待,占有鎖的客戶端將在鎖有效時間過后自動失去鎖浩螺,這時其他客戶端就可以占有該鎖,這樣將出現(xiàn)兩個客戶端同時占有一個鎖的情況仍侥,分布式鎖失效了要出。

所以,該方案鎖的有效時間就非常重要农渊,鎖的有效時間設(shè)置過短患蹂,可能會出現(xiàn)分布式鎖失效的情況,而有效時間設(shè)置過長,那么占有鎖的客戶端下線后传于,其他客戶端仍然要無效等待較長時間才可以占有該鎖囱挑,性能較差。
有沒有更好一點(diǎn)的方案沼溜?我們看一下方案2平挑。

方案2:自動延遲鎖有效時間。
我們可以在一開始給鎖設(shè)置一個較短的有效時間系草,并啟動一個后臺線程弹惦,在該鎖失效前,主動延遲該鎖的有效時間悄但,
例如棠隐,在一開始時給鎖設(shè)置有效時間為10秒,并啟動一個后臺線程檐嚣,每隔9秒助泽,就將鎖的過期時間修改為當(dāng)前時間10秒后。
示意代碼如下:

new Thread(new Runnable() {
    public void run() {
        while(lockIsExist) {
            redis.call("EXPIRE user_lock 10");
            Thread.sleep(1000 * 9);
        }
    }
}).start();

這樣就可以保證當(dāng)前占用客戶端的鎖不會因為時間到期而失效嚎京,避免了分布式鎖失效的問題嗡贺,并且如果當(dāng)前客戶端故障下線,由于沒有后臺線程定時延遲鎖有效時間鞍帝,該鎖也會很快自動失效诫睬。
提示;當(dāng)前客戶端釋放鎖的時候帕涌,需要停止該后臺線程或者修改lockIsExist為false摄凡。

Java客戶端Redisson提供了該方案,使用非常方便蚓曼。
下面介紹一下如何示意Redisson實現(xiàn)Redis分布式鎖亲澡。
(1)添加Redisson引用。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.4</version>
</dependency>

(2)使用示例如下:

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redissonClient = Redisson.create(config);

RLock lock = redissonClient.getLock("user_lock");
lock.lock();
try {
// process...
} finally {
    lock.unlock();
}

如果沒有特殊原因纫版,建議直接使用Redisson提供的分布式鎖床绪。

但這種方式就一定安全嗎?
大家考慮這樣一種場景其弊,假如獲得鎖的客戶端因為CPU負(fù)載過高或者GC等原因癞己,負(fù)責(zé)延遲鎖過期時間的線程沒法按時獲得CPU去執(zhí)行任務(wù),則同樣會出現(xiàn)鎖失效的場景梭伐。

picture 2

該場景暫時沒有比較好的處理方案痹雅,也不展開。

Sentinel籽御、Cluster模式下實現(xiàn)分布式鎖

實際生產(chǎn)環(huán)境中比較少使用單節(jié)點(diǎn)的Redis练慕,通常會部署Sentinel惰匙、Cluster模型部署Redis集群技掏,Redis在這兩種模式下線實現(xiàn)分布式鎖會有一個很麻煩的問題了铃将。
為了保證高性能,Redis主從同步使用的是異步模式哑梳,就是說Redis主節(jié)點(diǎn)返回SET命令成功響應(yīng)時劲阎,Redis從節(jié)點(diǎn)可能還沒有同步該命令。
如果這時主節(jié)點(diǎn)故障下線了鸠真,那么就會出現(xiàn)以下情況:
(1)Sentinel悯仙、Cluster模式會選舉一個從節(jié)點(diǎn)成為新主節(jié)點(diǎn),而這個主節(jié)點(diǎn)是沒有執(zhí)行SET命令的吠卷。也就是說這時客戶端并沒有占有鎖锡垄。
(2)客戶端收到(之前主節(jié)點(diǎn)返回的)SET命令的成功響應(yīng),以為自己占有鎖成功祭隔。
這時其他客戶端也請求這個鎖货岭,也能占有這個鎖,這時就會出現(xiàn)分布式鎖失效的情況疾渴。

picture 3

出現(xiàn)這個情況的本質(zhì)是Redis使用了異步復(fù)制的方式同步主從節(jié)點(diǎn)數(shù)據(jù)千贯,并不嚴(yán)格保證主從節(jié)點(diǎn)數(shù)據(jù)的一致性。
對此搞坝,Redis作者提出了RedLock算法搔谴,大概方案是部署多個單獨(dú)的Redis主節(jié)點(diǎn),并將SET命令同時發(fā)送到多個節(jié)點(diǎn)桩撮,當(dāng)收到半數(shù)以上Redis主節(jié)點(diǎn)返回成功后敦第,則認(rèn)為加鎖成功。
這種機(jī)制感覺與分布式一致性算法(如Raft算法)中利用的“Quorum機(jī)制”基本一致吧店量。
關(guān)于該方案是否能真正保證分布式鎖安全申尼,Redis作者與另一位大佬Martin爆發(fā)了熱烈的討論,本文偏向?qū)崙?zhàn)內(nèi)容垫桂,這里不一一展示RedLock算法細(xì)節(jié)师幕。

即使該算法可以真正保證分布式鎖安全,如果你要使用該方案诬滩,也很麻煩霹粥,需要另外部署多個Redis主節(jié)點(diǎn),還需要支持該算法的可靠的客戶端疼鸟『罂兀考慮這些情況,如果在嚴(yán)格要求分布式鎖安全的情況空镜,使用ZooKeeper浩淘、Etcd等嚴(yán)格保證數(shù)據(jù)一致性的組件更合適捌朴。

Zookeeper分布式鎖

Zookeeper由于保證集群數(shù)據(jù)一致,并自帶Watch张抄,客戶端過期失效檢測等機(jī)制砂蔽,非常適合實現(xiàn)分布式鎖。
Zookeeper實現(xiàn)分布式鎖的方式很簡單署惯,客戶端通過創(chuàng)建臨時節(jié)點(diǎn)來鎖定分布式鎖左驾,如果創(chuàng)建成功,則加鎖成功极谊,否則诡右,說明該鎖已經(jīng)被其他客戶端鎖定,這時當(dāng)前客戶端監(jiān)聽該臨時節(jié)點(diǎn)變化轻猖,如果該臨時節(jié)點(diǎn)被刪除帆吻,則可以再次嘗試鎖定該分布式鎖。
雖然ZooKeeper實現(xiàn)分布鎖的不同方案細(xì)節(jié)不同咙边,但整體基本基于該方案進(jìn)行擴(kuò)展猜煮。

這里推薦使用Curator框架(Netflix提供的ZooKeeper客戶端)實現(xiàn)分布式鎖,非常方便样眠。
下面介紹一下Curator的使用友瘤。
(1)引入Curator引用

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>3.3.0</version>
</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>3.3.0</version>
</dependency>

注意Curator版本與ZooKeeper版本對應(yīng)。

(2)使用InterProcessMutex類實現(xiàn)分布式鎖檐束。

CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", 60000, 15000,
        new ExponentialBackoffRetry(1000, 3));
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/user_lock");
lock.acquire();
try {
    // process...
} finally {
    lock.release();
}

Curator支持多種分布式鎖辫秧,非常全面:

  • InterProcessMutex:可重入排它鎖,例子展示就是這種鎖被丧。
  • InterProcessSemaphoreMutex:不可重入排它鎖盟戏。
  • InterProcessReadWriteLock:分布式讀寫鎖。
    使用方式也非常簡單甥桂,這里不一一展開柿究。

那么Zookeeper實現(xiàn)分布式鎖一定安全嗎?
假如客戶端Client1在ZooKeeper中加鎖成功黄选,即成功創(chuàng)建了臨時ZK節(jié)點(diǎn)蝇摸,但Client1由于GC長時間沒有響應(yīng)ZooKeeper的心跳檢測請求,ZooKeeper將Client1判斷為失效办陷,從而將臨時ZK節(jié)點(diǎn)貌夕,這時客戶端Client2請求加鎖就可以成功加鎖。那么這時就會出現(xiàn)Client1民镜、Client2同時占有一個分布式鎖啡专,即分布式鎖失效。
該場景與上面說的Redis延遲線程沒有按時執(zhí)行的場景有點(diǎn)類型制圈,該場景展示也沒有較好的解決方案们童。
雖然理論上ZooKeeper存在分布式鎖失效的可能畔况,但發(fā)生的概率應(yīng)該比較,也可以通過增加ZooKeeper判斷客戶端的時間來減少這種場景慧库,所以ZooKeeper分布式鎖是可以滿足絕大數(shù)要求分布式鎖的場景的跷跪。

總結(jié)一下:
(1)
如果不嚴(yán)格要求分布式鎖安全,可以考慮在Sentinel完沪、Cluster模式下使用redis實現(xiàn)分布式鎖域庇。例如多個客戶端同時獲取鎖并不會導(dǎo)致嚴(yán)重的業(yè)務(wù)問題嵌戈,或者只是要求性能優(yōu)化避免多個客戶端同時操作等場景覆积,都可以使用Redisson提供的分布式鎖。
(2)如果嚴(yán)格要求分布式鎖安全熟呛,則可以使用ZooKeeper宽档、Etcd等組件實現(xiàn)分布式鎖。
當(dāng)然庵朝,建議使用Redisson吗冤、Curator等成熟框架實現(xiàn)分布式鎖,避免重復(fù)編碼九府,也減少錯誤風(fēng)險椎瘟。

如需系統(tǒng)學(xué)習(xí)Redis,可參考作者新書《Redis核心原理與實踐》侄旬。本書通過深入分析Redis 6.0源碼肺蔚,總結(jié)了Redis核心功能的設(shè)計與實現(xiàn)。通過閱讀本書儡羔,讀者可以深入理解Redis內(nèi)部機(jī)制及最新特性宣羊,并學(xué)習(xí)到Redis相關(guān)的數(shù)據(jù)結(jié)構(gòu)與算法、Unix編程汰蜘、存儲系統(tǒng)設(shè)計仇冯,分布式系統(tǒng)架構(gòu)等一系列知識。
經(jīng)過該書編輯同意族操,我會繼續(xù)在個人技術(shù)公眾號(binecy)發(fā)布書中部分章節(jié)內(nèi)容苛坚,作為書的預(yù)覽內(nèi)容,歡迎大家查閱色难,謝謝泼舱。

語雀平臺預(yù)覽:《Redis核心原理與實踐》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市莱预,隨后出現(xiàn)的幾起案子柠掂,更是在濱河造成了極大的恐慌,老刑警劉巖依沮,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涯贞,死亡現(xiàn)場離奇詭異枪狂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)宋渔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門州疾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人皇拣,你說我怎么就攤上這事严蓖。” “怎么了氧急?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵颗胡,是天一觀的道長。 經(jīng)常有香客問我吩坝,道長毒姨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任钉寝,我火速辦了婚禮弧呐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嵌纲。我一直安慰自己俘枫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布逮走。 她就那樣靜靜地躺著鸠蚪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪言沐。 梳的紋絲不亂的頭發(fā)上邓嘹,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機(jī)與錄音险胰,去河邊找鬼汹押。 笑死,一個胖子當(dāng)著我的面吹牛起便,可吹牛的內(nèi)容都是我干的棚贾。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼榆综,長吁一口氣:“原來是場噩夢啊……” “哼妙痹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鼻疮,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤怯伊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后判沟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耿芹,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崭篡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吧秕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琉闪。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖砸彬,靈堂內(nèi)的尸體忽然破棺而出颠毙,到底是詐尸還是另有隱情,我是刑警寧澤砂碉,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布蛀蜜,位于F島的核電站,受9級特大地震影響绽淘,放射性物質(zhì)發(fā)生泄漏涵防。R本人自食惡果不足惜闹伪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一沪铭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧偏瓤,春花似錦杀怠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至证舟,卻和暖如春硕旗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背女责。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工漆枚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抵知。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓墙基,卻偏偏與公主長得像,于是被迫代替她去往敵國和親刷喜。 傳聞我的和親對象是個殘疾皇子残制,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

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