溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何鎖以及分布式鎖

發(fā)布時間:2022-01-15 17:40:04 來源:億速云 閱讀:92 作者:柒染 欄目:大數(shù)據(jù)

如何鎖以及分布式鎖,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。

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

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

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

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

鎖的類型

1 自旋鎖 (Spin Lock)

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

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

Java沒有默認的自旋鎖實現(xiàn),示例代碼如下:

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);
 }
}

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

2 互斥鎖 (Mutex Lock)

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

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

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

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

如果是多核處理器,如果預(yù)計線程等待鎖的時間較長,至少比兩次線程上下文切換的時間要長,建議使用互斥鎖。

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

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

3 可重入鎖 (Reentrant Lock)

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

  1. 首先它是互斥鎖:任意時刻,只有一個線程鎖。即假設(shè)A線程已經(jīng)獲取了鎖,在A線程釋放這個鎖之前,B線程是無法獲取到這個鎖的,B要獲取這個鎖就會進入阻塞狀態(tài)。

  2. 其次,它可以被同一個線程多次持有。即,假設(shè)A線程已經(jīng)獲取了這個鎖,如果A線程在釋放鎖之前又一次請求獲取這個鎖,那么是能夠獲取成功的。

Java中的synchronized, ReentrantLock都是可重入鎖。

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

首先互斥是一種會導致線程掛起,并在較短時間內(nèi)又需要重新調(diào)度回原線程的,較為消耗資源的操作。

Java6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java6里鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

數(shù)據(jù)庫中針對不同的鎖層級(Lock Hierarchy,表/頁/行等),
也有類似鎖升級(Lock Escalations)的理念。

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

鎖的后遺癥

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

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

分布式鎖

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

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

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

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

分布式鎖的設(shè)計要點

我們以Redis為例,簡單思考下這個鎖的實現(xiàn)。
似乎加鎖的時候只要一個 SETNX 命令就搞定了,返回1代表加鎖成功,返回0 表示鎖被占用著。然后再用 DEL 命令解鎖,返回1表示解鎖成功,0表示已經(jīng)被解鎖過。
接著問題就來了:
SETNX會存在鎖競爭,如果在執(zhí)行過程中客戶端宕機,會引起死鎖問題,也就是鎖資源無法釋放。解決死鎖的問題其實可以可以向Mysql的死鎖檢測學習,設(shè)置一個失效時間,通過key的時間戳來判斷是否需要強制解鎖。
但是強制解鎖也存在問題,一個就是時間差問題,不同的機器的本地時間可能也存在時間差,在很小事務(wù)粒度的高并發(fā)場景下還是會存在問題,比如刪除鎖的時候,會判斷時間戳已經(jīng)超過時效,有可能刪除其他已經(jīng)獲取鎖的客戶端的鎖。
另外,如果設(shè)置了一個超時時間,若程序執(zhí)行時間超過了超時時間,那么還沒執(zhí)行完鎖會被自動釋放,原來持鎖的客戶端再次解鎖的時候會出現(xiàn)問題,而且最為嚴重的還是一致性沒有得到保障。如何合理的設(shè)置這個超時時間可能是一個觀測并不斷調(diào)整的過程。

那么,總結(jié)下設(shè)計的幾個要點:

  • 鎖的時效。避免單點故障造成死鎖,影響其他客戶端獲取鎖。但是也要保證一旦一個客戶端持鎖,在客戶端可用時不會被其他客戶端解鎖。

  • 持鎖期間的check。盡量在關(guān)鍵節(jié)點檢查鎖的狀態(tài),所以要設(shè)計成可重入鎖。

  • 減少獲取鎖的操作,盡量減少redis壓力。所以需要讓客戶端的申請鎖有一個等待時間,而不是所有申請鎖的請求線程不斷的循環(huán)申請鎖。

  • 加鎖的事務(wù)或者操作盡量粒度小,減少其他客戶端申請鎖的等待時間,提高處理效率和并發(fā)性。

  • 持鎖的客戶端解鎖后,要能通知到其他等待鎖的節(jié)點,否則其他節(jié)點只能一直等待一個預(yù)計的時間再觸發(fā)申請鎖。類似線程的notifyAll,要能同步鎖狀態(tài)給其他客戶端,并且是分布式消息。

  • 考慮任何執(zhí)行句柄中可能出現(xiàn)的異常,狀態(tài)的正確流轉(zhuǎn)和處理。比如,不能因為一個節(jié)點解鎖失敗,或者鎖查詢失?。╮edis 超時或者其他運行時異常),影響整個等待的任務(wù)隊列,或者任務(wù)池。

  • 若Redis服務(wù)器宕機或者網(wǎng)絡(luò)異常,要有其他備份方案,比如單機鎖限流+最終數(shù)據(jù)庫的持久化鎖來做好最終一致性控制。

看完上述內(nèi)容,你們掌握如何鎖以及分布式鎖的方法了嗎?如果還想學到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI