[TOC]
1. 分布式鎖背景
在單體機器的jvm中,多個線程想要訪問共享資源兴想,那么,需要在jvm中創(chuàng)建一個獨占鎖赡勘,哪個線程獲取到了鎖嫂便,那么這個線程可以訪問資源。其他線程只能等待獲取到鎖的線程釋放鎖闸与。
在多體機器的集群環(huán)境中毙替,仍然是多個線程想要訪問共享資源。但是因為這些線程并不在一個jvm中践樱,所以創(chuàng)建獨占鎖就不能實現(xiàn)不同機器的jvm內(nèi)的線程等待厂画。所以就需要引入第三方鎖,集群內(nèi)的所有jvm都可以訪問第三方鎖拷邢,哪一個機器的得到了鎖袱院,那么這個機器的線程就可以訪問資源,其他機器的線程只能等待釋放鎖瞭稼。
2. 鎖的基本特性
- 安全:獨占鎖忽洛。在任意一個時刻,只有一個客戶端持有鎖环肘。
- 健壯:無死鎖欲虚。即使持有鎖的機器掛了,或者網(wǎng)絡(luò)不可達(dá)悔雹,也不能造成死鎖复哆。
- 容錯:只要存在一個可用的鎖平臺,那么就能獲取與釋放鎖腌零。
3. 基于Redis實現(xiàn)鎖的基本原理
實現(xiàn)redis分鎖的最簡單的方法就是在redis中創(chuàng)建一個key梯找,這個key有實效時間,保證分布式鎖的健壯性莱没,保證鎖最終會自動釋放初肉,不會出現(xiàn)死鎖。釋放鎖饰躲,就是刪除這個key牙咏。
上述實現(xiàn)看起來還不錯,但是依然存在問題:
假如獲得鎖的線程在超時時間內(nèi)還未處理完成怎么辦嘹裂?
假如redis集群主從復(fù)制失敗了怎么辦妄壶?
這兩個問題都會導(dǎo)致多個線程獲得了鎖,破壞了分布式鎖的安全性寄狼。
4. redis實現(xiàn)鎖
為了解決3中的兩個問題丁寄,可以隨機生成value.
也就是說氨淌,在獲取鎖的時候,使用set key value nx px time
來保證只有key不存在時伊磺,才會創(chuàng)建盛正,超時時間是time.超時時間就是線程持有鎖的最大時間。
釋放鎖的時候屑埋,需要驗證當(dāng)前線程釋放的線程是不是自己持有的鎖豪筝。
但是超時時間問題還是沒有解決。
使用set可以存儲字符串摘能,線程在獲取到鎖后续崖,將獲取鎖的時間做為值放入,同時還要加上線程自己的隨機數(shù)团搞,將字符串打造成多個屬性的對象的json串严望。
其他線程在獲取鎖的時候,根據(jù)json串逻恐,判斷像吻,持有鎖的線程是不是死掉了。
舉個例子:約定持有鎖的線程复隆,每隔1分鐘將json里面的值++萧豆。其他線程嘗試獲取鎖的時候,發(fā)現(xiàn)當(dāng)前時間已經(jīng)有多個時間間隔的值沒有更新了昏名,那么就可以認(rèn)為持有鎖的線程掛了。
5. redis分布式鎖
前面我們考慮的都是單體的redis如何實現(xiàn)分布式鎖阵面。
那么如果redis也是多個實例的轻局,這些實例之間完全獨立,沒有主從賦值或者其他集群協(xié)調(diào)样刷。那么前面我們討論的解決方案就不能保證安全了仑扑。
為了實現(xiàn)分布式鎖,我們可以約定==客戶端嘗試向所有的redis實例獲取鎖置鼻,如果至少有2/3的redis獲取鎖成功镇饮,那么就表示這個客戶端獲取分布式鎖成功。鎖需要時間戳和隨機值保證唯一性箕母。==
因為我們的閾值是2/3储藐,不可能同時有多個線程獲取2/3的鎖,而且這些鎖還是同一把鎖嘶是。
為了防止redis實例不可達(dá)钙勃,我們不僅僅需要2/3成功,還需要在獲取鎖的時候聂喇,設(shè)置小于2個數(shù)量級的超時時間辖源。
舉個例子:
我們redis實例有5個,這些redis實例之間沒沒有任何關(guān)系。
接著客戶端得到鎖的key==(所有的鎖的key相同克饶,value不同)==酝蜒。
然后客戶端嘗試向所有的redis實例注冊鎖。
假設(shè)有3個redis實例注冊成功矾湃,此時客戶端持有鎖亡脑。
另一個客戶端在第一個客戶端持有鎖的狀態(tài)下,嘗試獲取鎖洲尊,那么远豺,此時至少有2/3的redis實例的鎖是占有的,那么嘗試獲取鎖的線程就無法滿足2/3的這個閾值了坞嘀,就無法持有鎖了躯护。
嘗試獲取鎖失敗,需要盡快釋放已經(jīng)獲取持有鎖的redis實例丽涩,避免影響下一次獲取鎖棺滞。
假設(shè)鎖的有效時間是10s,那么客戶端和redis的連接超時時間應(yīng)該設(shè)置為100ms <= 在兩個數(shù)量級以上矢渊,否則線程花費80%以上的時間獲取了鎖继准,然后還沒開始使用呢湃密,就超時了薛匪。
==超時續(xù)期==可以使用==EXPIRE==進行續(xù)期。
這個方法能滿足需要遭殉,但是依然不太好毡鉴,因為嘗試獲取鎖的時候崔泵,不是同步的,也就是說猪瞬,無法在同一時間獲取到全部2/3的鎖憎瘸。獲取鎖的過程中,也需要花費一定的時間陈瘦。
所以幌甘,鎖的實際使用時間是不確定的,即使有超時時間痊项,實際可使用的時間也是小于超時時間的锅风。
而且,還存在一個比較致命的問題鞍泉,這些redis實例之間存在==時鐘漂移==遏弱。當(dāng)redis實例之間沒有做時鐘同步,那么因為時鐘漂移問題塞弊,會造成鎖的實際使用時間很可能是不確定的漱逸,往往小于預(yù)期時間泪姨。
6. 獲取鎖失敗
當(dāng)客戶端獲取鎖失敗后,不應(yīng)該立即重試饰抒,一般情況下肮砾,如果因為沖突而無法獲取到鎖,那么失敗后立即重試袋坑,幾乎也是失敗的仗处。因為多個客戶端在同一時間搶奪同一個鎖,會造成==腦裂==枣宫。(為了防止腦裂婆誓,一般解決方式是采用過半策略。得到支持的數(shù)量超過一半才能認(rèn)為是得到整個集群的支持)
所以也颤,客戶端在獲取鎖失敗后洋幻,應(yīng)該等待隨機的時間,然后在嘗試獲取鎖翅娶。
而且還應(yīng)該注意一點文留,當(dāng)獲取失敗后,應(yīng)該盡可能快的釋放已經(jīng)獲取到的鎖竭沫。否則燥翅,在一個超時時間內(nèi),沒有客戶端可以獲取鎖蜕提。
還是延續(xù)前面的例子:我們有5個客戶端森书,5個redis實例。
第一次:每個客戶端得到了一個redis鎖谎势,但是沒有客戶端獲取的redis鎖的數(shù)量超過2/3拄氯,所有客戶端獲取鎖失敗。
第二次:因為客戶端等待隨機的時間它浅,有2個客戶端獲取到了鎖,另外有一個客戶端獲取到了鎖镣煮,其他兩個客戶端沒有獲取到鎖姐霍,因為不滿足2/3的策略,獲取失敗典唇。
多次進行后镊折,到達(dá)了超時時間,依然沒有客戶端獲取到鎖介衔,那么恨胚,這個鎖就是低可用性的鎖,特別是隨著客戶端的數(shù)量的增加炎咖,可用性也會下降赃泡。
==失敗懲罰==
某個客戶端嘗試獲取鎖寒波,當(dāng)?shù)玫?/3的鎖后,發(fā)現(xiàn)剩余的鎖都被占用了升熊,此時客戶端無法獲取鎖俄烁,需要釋放,結(jié)果在釋放一半的時候级野,網(wǎng)絡(luò)中斷了页屠,那么這個客戶端持有的鎖在超時時間內(nèi)就無法釋放了。只能等到超時時間到蓖柔,自動釋放辰企。
==此時這些鎖可以認(rèn)為在這段時間內(nèi)被懲罰了。==
7. 最終
這樣就完美了嗎况鸣?
當(dāng)然不是牢贸,我們前面的==過半策略==是==2/3==如果更小點呢?
假設(shè)現(xiàn)在有100個redis實例懒闷,我們的閾值是60%十减。
因為持續(xù)并發(fā),需要增加redis實例愤估,于是又增加了100個redis實例帮辟。
如果在增加的同時,正好有客戶端在獲取鎖玩焰,那么此時由驹,就有可能存在多個客戶端獲取到鎖的問題。
所以昔园,這個過半策略蔓榄,應(yīng)該是能夠動態(tài)計算的。
- redis實例崩潰造成鎖在一定的時間內(nèi)不可用
即使這樣默刚,在分布式環(huán)境下甥郑,存在著各種各樣的問題,比如redis實例崩潰荤西,導(dǎo)致鎖本來是空閑的澜搅,但是集群內(nèi)的部分redis實例崩潰了,在進行重啟恢復(fù)的時候邪锌,只恢復(fù)到了鎖持有的狀態(tài)勉躺,此時如果崩潰的機器數(shù)量比較大,就會導(dǎo)致在這部分崩潰的機器的鎖自動釋放前觅丰,沒有任何客戶端可以獲取鎖饵溅。
- 因網(wǎng)絡(luò)隔離,造成鎖不安全
假設(shè)我們有100個redis實例妇萄,客戶端A現(xiàn)在已經(jīng)獲取到了2/3的鎖66個蜕企,此時咬荷,集群的鎖是占用狀態(tài)。
但是因為動態(tài)削減redis實例糖赔,造成B客戶端在嘗試獲取鎖的時候萍丐,獲取了33個鎖,就滿足過半策略了(假設(shè)從100 -> 48),此時33剛好是48的2/3放典,那么就相當(dāng)于兩個客戶端都獲取到了鎖逝变。
8. 使用
在java語言中,使用的最多的redis鎖應(yīng)該就是redisson了奋构。