理解鎖以及分布式鎖

分布式鎖 distributed locks

distributedlocks.png

資源有限锯蛀,爭搶難免什猖,最簡單粗暴的辦法是誰的拳頭大誰就可以搶到最好的資源薇组。那在計算機世界里是如何搶奪資源(cpu, 內存, 網(wǎng)絡等)的呢肿仑?

在多線程的軟件世界里序目,對共享資源的爭搶過程(Data Race)就是并發(fā)坑律,而對共享資源數(shù)據(jù)進行訪問保護的最直接辦法就是引入岩梳!。

POSIX threads(簡稱Pthreads)是在多核平臺上進行并行編程的一套常用的API晃择。線程同步(Thread Synchronization)是并行編程中非常重要的通訊手段冀值,其中最典型的應用就是用Pthreads提供的鎖機制(lock)來對多個線程之間共 享的臨界區(qū)(Critical Section)進行保護(另一種常用的同步機制是barrier)。

無鎖編程也是一種辦法宫屠,但它不在本文的討論范圍列疗,并發(fā)多線程轉為單線程(Disruptor),函數(shù)式編程浪蹂,鎖粒度控制(ConcurrentHashMap桶)抵栈,信號量(Semaphore)等手段都可以實現(xiàn)無鎖或鎖優(yōu)化。

技術上來說坤次,鎖也可以理解成將大量并發(fā)請求串行化古劲,但請注意串行化不能簡單等同為** 排隊 ,因為這里和現(xiàn)實世界沒什么不同缰猴,排隊意味著大家是公平Fair的領到資源产艾,先到先得,然而很多情況下為了性能考量多線程之間還是會不公平Unfair**的去搶。Java中ReentrantLock可重入鎖闷堡,提供了公平鎖和非公平鎖兩種實現(xiàn)

再注意一點隘膘,串行也不是意味著只有一個排隊的隊伍,每次只能進一個缚窿。當然可以好多個隊伍棘幸,每次進入多個焰扳。比如餐館一共10個餐桌倦零,服務員可能一次放行最多10個人進去,有人出來再放行同數(shù)量的人進去吨悍。Java中Semaphore信號量扫茅,相當于同時管理一批鎖

鎖的類型

1 自旋鎖 (Spin Lock)

自旋鎖如果已經(jīng)被別的線程獲取,調用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖育瓜,”自旋”一詞就是因此而得名葫隙。

自旋鎖是一種非阻塞鎖,也就是說躏仇,如果某線程需要獲取自旋鎖恋脚,但該鎖已經(jīng)被其他線程占用時,該線程不會被掛起焰手,而是在不斷的消耗CPU的時間糟描,不停的試圖獲取自旋鎖。

Java沒有默認的自旋鎖實現(xiàn)书妻,示例代碼如下:
<pre>
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
</pre>

通過示例船响,可以看到CAS原子操作將sign從期望的null設置為當前線程,線程A第一次調用lock()可以獲取鎖躲履,第二次調用將進入循環(huán)等待见间,因為sign已經(jīng)被設置為了current。
簡單加個當前鎖的owner比對判斷和鎖計數(shù)器工猜,即可實現(xiàn)重入米诉。

2 互斥鎖 (Mutex Lock)

互斥鎖是阻塞鎖,當某線程無法獲取互斥鎖時篷帅,該線程會被直接掛起荒辕,不再消耗CPU時間,當其他線程釋放互斥鎖后犹褒,操作系統(tǒng)會喚醒那個被掛起的線程抵窒。

阻塞鎖可以說是讓線程進入阻塞狀態(tài)進行等待,當獲得相應的信號(喚醒叠骑,時間)時李皇,才可以進入線程的準備就緒狀態(tài),準備就緒狀態(tài)的所有線程,通過競爭進入運行狀態(tài)掉房。它的優(yōu)勢在于茧跋,阻塞的線程不會占用 CPU 時間, 不會導致 CPU 占用率過高卓囚,但進入時間以及恢復時間都要比自旋鎖略慢瘾杭。在競爭激烈的情況下阻塞鎖的性能要明顯高于自旋鎖。

<pre>
JAVA中哪亿,能夠進入/退出粥烁、阻塞狀態(tài)或包含阻塞鎖的方法有:
synchronized
ReentrantLock
Object.wait()/notify()
LockSupport.park()/unpart()(j.u.c經(jīng)常使用)
</pre>

自旋鎖 VS 互斥鎖
兩種鎖適用于不同場景:
如果是多核處理器,預計線程等待鎖的時間很短蝇棉,短到比線程兩次上下文切換時間要少的情況下讨阻,使用自旋鎖是劃算的。

如果是多核處理器篡殷,如果預計線程等待鎖的時間較長钝吮,至少比兩次線程上下文切換的時間要長,建議使用互斥鎖板辽。

如果是單核處理器奇瘦,一般建議不要使用自旋鎖。因為劲弦,在同一時間只有一個線程是處在運行狀態(tài)耳标,那如果運行線程發(fā)現(xiàn)無法獲取鎖,只能等待解鎖瓶您,但因為自身不掛起麻捻,所以那個獲取到鎖的線程沒有辦法進入運行狀態(tài),只能等到運行線程把操作系統(tǒng)分給它的時間片用完呀袱,才能有機會被調度贸毕。這種情況下使用自旋鎖的代價很高。

如果加鎖的代碼經(jīng)常被調用夜赵,但競爭情況很少發(fā)生時明棍,應該優(yōu)先考慮使用自旋鎖,自旋鎖的開銷比較小寇僧,互斥量的開銷較大摊腋。

3 可重入鎖 (Reentrant Lock)

可重入鎖是一種特殊的互斥鎖,它可以被同一個線程多次獲取嘁傀,而不會產(chǎn)生死鎖兴蒸。

  1. 首先它是互斥鎖:任意時刻,只有一個線程鎖细办。即假設A線程已經(jīng)獲取了鎖橙凳,在A線程釋放這個鎖之前,B線程是無法獲取到這個鎖的,B要獲取這個鎖就會進入阻塞狀態(tài)岛啸。
  2. 其次钓觉,它可以被同一個線程多次持有。即坚踩,假設A線程已經(jīng)獲取了這個鎖荡灾,如果A線程在釋放鎖之前又一次請求獲取這個鎖,那么是能夠獲取成功的瞬铸。

<pre>Java中的synchronized, ReentrantLock都是可重入鎖批幌。</pre>

4 輕量級鎖(Lightweight Lock) & 偏向鎖(Biased Lock)

首先互斥是一種會導致線程掛起,并在較短時間內又需要重新調度回原線程的赴捞,較為消耗資源的操作逼裆。

Java6為了減少獲得鎖和釋放鎖所帶來的性能消耗郁稍,引入了“偏向鎖”和“輕量級鎖”赦政,所以在Java6里鎖一共有四種狀態(tài),無鎖狀態(tài)耀怜,偏向鎖狀態(tài)恢着,輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會隨著競爭情況逐漸升級财破。鎖可以升級但不能降級掰派,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略左痢,目的是為了提高獲得鎖和釋放鎖的效率靡羡。
<pre>數(shù)據(jù)庫中針對不同的鎖層級(Lock Hierarchy,表/頁/行等)俊性,
也有類似鎖升級(Lock Escalations)的理念略步。</pre>

5 JUC

并發(fā)大師Doug Lea在JUC包中實現(xiàn)了大量的并發(fā)工具類,并發(fā)思想在源碼中得到了很好的體現(xiàn)定页。比如Semaphore, CountDownLatch, CyclicBarrier都是特定場景下的經(jīng)典實現(xiàn)趟薄,大家有興趣可以自行研究,最終一嘆:** 鎖 **原來可以玩出這么多花樣來典徊。

java-7-concurrent-executors-uml-class-diagram-example.png

鎖的后遺癥

在并發(fā)世界里杭煎,鎖扮演了一個個亦正亦邪的角色,甚至很多時候是個大反派卒落。鎖的后遺癥包括:死鎖羡铲,饑餓,活鎖儡毕,Lock Convoying(多個同優(yōu)先級的線程重復競爭同一把鎖也切,此時大量雖然被喚醒而得不到鎖的線程被迫進行調度切換,這種頻繁的調度切換相當影響系統(tǒng)性能),優(yōu)先級反轉贾费,不公平和低效率等钦购。而這些問題都是在實現(xiàn)鎖的過程中普遍存在而又不得不面對的。
這里只拋出問題讓讀者了解褂萧,具體解決方案不在本文范疇押桃。

活鎖和死鎖的區(qū)別在于,處于活鎖的實體是在不斷的改變狀態(tài)导犹,所謂之“活”唱凯, 而處于死鎖的實體表現(xiàn)為等待;活鎖有可能自行解開谎痢,死鎖則不能磕昼。

分布式鎖

相對于單機應用設定的單機鎖,為分布式應用各節(jié)點對共享資源的排他式訪問而設定的鎖就是分布式鎖节猿。在分布式場景下票从,有很多種情況都需要實現(xiàn)多節(jié)點的最終一致性。比如全局發(fā)號器滨嘱,分布式事務等等峰鄙。

傳統(tǒng)實現(xiàn)分布式鎖的方案一般是利用持久化數(shù)據(jù)庫(如利用InnoDB行鎖,或事務太雨,或version樂觀鎖)吟榴,當然大部分時候可以滿足大部分人的需求。而如今互聯(lián)網(wǎng)應用的量級已經(jīng)幾何級別的爆發(fā)囊扳,利用諸如zookeeper吩翻,redis等更高效的分布式組件來實現(xiàn)分布式鎖,可以提供高可用的更強壯的鎖特性锥咸,并且支持豐富化的使用場景狭瞎。
開源實現(xiàn)已有不少比如Redis作者基于Redis設計的Redlock,Redission等她君。

小插曲:
鎖存在的地方就有爭議脚作,Redlock也不例外。有一位分布式專家曾經(jīng)發(fā)表過一片文章<How to do distributed locking>, 質疑Redlock的正確性缔刹,Redis作者則在<Is Redlock safe?>中給予了回應球涛,爭鋒相對精彩無比,有興趣的讀者可以自行前往校镐。

前人栽樹后人乘涼亿扁,當下各種的鎖實現(xiàn)已經(jīng)給我們提供了很多優(yōu)雅的設計范本,我們具體來分析下分布式鎖到底應該怎么設計呢鸟廓?

分布式鎖的設計要點

我們以Redis為例从祝,簡單思考下這個鎖的實現(xiàn)襟己。
似乎加鎖的時候只要一個*** SETNX 命令就搞定了,返回1代表加鎖成功牍陌,返回0 表示鎖被占用著擎浴。然后再用 DEL ***命令解鎖,返回1表示解鎖成功毒涧,0表示已經(jīng)被解鎖過贮预。
接著問題就來了:
SETNX會存在鎖競爭,如果在執(zhí)行過程中客戶端宕機契讲,會引起死鎖問題仿吞,也就是鎖資源無法釋放。解決死鎖的問題其實可以可以向Mysql的死鎖檢測學習捡偏,設置一個失效時間唤冈,通過key的時間戳來判斷是否需要強制解鎖。
但是強制解鎖也存在問題银伟,一個就是時間差問題你虹,不同的機器的本地時間可能也存在時間差,在很小事務粒度的高并發(fā)場景下還是會存在問題枣申,比如刪除鎖的時候售葡,會判斷時間戳已經(jīng)超過時效看杭,有可能刪除其他已經(jīng)獲取鎖的客戶端的鎖忠藤。
另外,如果設置了一個超時時間楼雹,若程序執(zhí)行時間超過了超時時間模孩,那么還沒執(zhí)行完鎖會被自動釋放,原來持鎖的客戶端再次解鎖的時候會出現(xiàn)問題贮缅,而且最為嚴重的還是一致性沒有得到保障榨咐。如何合理的設置這個超時時間可能是一個觀測并不斷調整的過程。

那么谴供,總結下設計的幾個要點:

  • 鎖的時效块茁。避免單點故障造成死鎖,影響其他客戶端獲取鎖桂肌。但是也要保證一旦一個客戶端持鎖数焊,在客戶端可用時不會被其他客戶端解鎖。
  • 持鎖期間的check崎场。盡量在關鍵節(jié)點檢查鎖的狀態(tài)佩耳,所以要設計成可重入鎖。
  • 減少獲取鎖的操作谭跨,盡量減少redis壓力干厚。所以需要讓客戶端的申請鎖有一個等待時間李滴,而不是所有申請鎖的請求線程不斷的循環(huán)申請鎖。
  • 加鎖的事務或者操作盡量粒度小蛮瞄,減少其他客戶端申請鎖的等待時間所坯,提高處理效率和并發(fā)性。
  • 持鎖的客戶端解鎖后挂捅,要能通知到其他等待鎖的節(jié)點包竹,否則其他節(jié)點只能一直等待一個預計的時間再觸發(fā)申請鎖。類似線程的notifyAll,要能同步鎖狀態(tài)給其他客戶端籍凝,并且是分布式消息周瞎。
  • 考慮任何執(zhí)行句柄中可能出現(xiàn)的異常,狀態(tài)的正確流轉和處理饵蒂。比如声诸,不能因為一個節(jié)點解鎖失敗,或者鎖查詢失斖硕ⅰ(redis 超時或者其他運行時異常)彼乌,影響整個等待的任務隊列,或者任務池渊迁。
  • 若Redis服務器宕機或者網(wǎng)絡異常慰照,要有其他備份方案,比如單機鎖限流+最終數(shù)據(jù)庫的持久化鎖來做好最終一致性控制琉朽。

如果大家想自己實現(xiàn)分布式鎖的話毒租,建議先把開源的一些實現(xiàn)先讀一讀,拓展下思路箱叁。
還是那句話墅垮,如非必要,勿增實體耕漱!

參考文章:
基于Redis實現(xiàn)分布式鎖算色,Redisson使用及源碼分析
聊一聊分布式鎖的設計

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市螟够,隨后出現(xiàn)的幾起案子灾梦,更是在濱河造成了極大的恐慌,老刑警劉巖妓笙,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件若河,死亡現(xiàn)場離奇詭異,居然都是意外死亡给郊,警方通過查閱死者的電腦和手機牡肉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淆九,“玉大人,你說我怎么就攤上這事∩巴悖” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵煌寇,是天一觀的道長。 經(jīng)常有香客問我逾雄,道長阀溶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任鸦泳,我火速辦了婚禮银锻,結果婚禮上,老公的妹妹穿的比我還像新娘做鹰。我一直安慰自己击纬,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布钾麸。 她就那樣靜靜地躺著更振,像睡著了一般。 火紅的嫁衣襯著肌膚如雪饭尝。 梳的紋絲不亂的頭發(fā)上肯腕,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音钥平,去河邊找鬼实撒。 笑死,一個胖子當著我的面吹牛帖池,可吹牛的內容都是我干的奈惑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼睡汹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寂殉?” 一聲冷哼從身側響起囚巴,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎友扰,沒想到半個月后彤叉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡村怪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年秽浇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甚负。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡柬焕,死狀恐怖审残,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情斑举,我是刑警寧澤搅轿,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站富玷,受9級特大地震影響璧坟,放射性物質發(fā)生泄漏。R本人自食惡果不足惜赎懦,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一雀鹃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧励两,春花似錦褐澎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至先鱼,卻和暖如春俭正,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背焙畔。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工掸读, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宏多。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓儿惫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伸但。 傳聞我的和親對象是個殘疾皇子肾请,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容

  • 引用自多線程編程指南應用程序里面多個線程的存在引發(fā)了多個執(zhí)行線程安全訪問資源的潛在問題。兩個線程同時修改同一資源有...
    Mitchell閱讀 1,992評論 1 7
  • 工作中經(jīng)常會遇到爭搶共享資源的場景更胖,比如用戶搶購秒殺商品铛铁,如果不對商品庫存進行保護,可能會造成超賣的情況却妨。超賣現(xiàn)象...
    南山羊閱讀 962評論 0 11
  • Java8張圖 11饵逐、字符串不變性 12、equals()方法彪标、hashCode()方法的區(qū)別 13倍权、...
    Miley_MOJIE閱讀 3,707評論 0 11
  • 一、多線程 說明下線程的狀態(tài) java中的線程一共有 5 種狀態(tài)捞烟。 NEW:這種情況指的是薄声,通過 New 關鍵字創(chuàng)...
    Java旅行者閱讀 4,680評論 0 44
  • 1 臨界區(qū) 1.1簡介 在早期計算機系統(tǒng)中当船,只有一個任務進程在執(zhí)行,并不存在資源的共享與競爭奸柬。隨著技術和需求的飛速...
    Fly晴天里Fly閱讀 9,036評論 2 13