悲觀鎖與樂觀鎖的實現(xiàn)(詳情圖解)
一、前言
1、在了解悲觀鎖和樂觀鎖之前,我們先了解一下什么是鎖,為什么要用到鎖?
2、技術(shù)來源于生活,鎖不僅在程序中存在,在現(xiàn)實中我們也隨處可見,例如我們上下班打卡的指紋鎖,保險柜上的密碼鎖,以及我們我們登錄的用戶名和密碼也是一種鎖,生活中用到鎖可以保護我們?nèi)松戆踩?指紋鎖)、財產(chǎn)安全(保險柜密碼鎖)、信息安全(用戶名密碼鎖),讓我們更放心的去使用和生活,因為有鎖,我們不用去擔(dān)心個人的財產(chǎn)和信息泄露。
3、而程序中的鎖,則是用來保證我們數(shù)據(jù)安全的機制和手段,例如當(dāng)我們有多個線程去訪問修改共享變量的時候,我們可以給修改操作加鎖(syncronized)。當(dāng)多個用戶修改表中同一數(shù)據(jù)時,我們可以給該行數(shù)據(jù)上鎖(行鎖)。因此,當(dāng)程序中可能出現(xiàn)并發(fā)的情況時,我們就需要通過一定的手段來保證在并發(fā)情況下數(shù)據(jù)的準確性,通過這種手段保證了當(dāng)前用戶和其他用戶一起操作時,所得到的結(jié)果和他單獨操作時的結(jié)果是一樣的
4、沒有做好并發(fā)控制,就可能導(dǎo)致臟讀、幻讀和不可重復(fù)讀等問題,如下圖所示:
由于并發(fā)操作,如果沒有加鎖進行并發(fā)控制,數(shù)據(jù)庫的最終的一條數(shù)據(jù)可能為3也有可能為5,導(dǎo)致數(shù)值不準確
二、悲觀鎖和樂觀鎖
首先我們需要清楚的一點就是無論是悲觀鎖還是樂觀鎖,都是人們定義出來的概念,可以認為是一種思想。
2.1、悲觀鎖
悲觀鎖(Pessimistic Lock): 就是很悲觀,每次去拿數(shù)據(jù)的時候都認為別人會修改。所以每次在拿數(shù)據(jù)的時候都會上鎖。這樣別人想拿數(shù)據(jù)就被擋住,直到悲觀鎖被釋放,悲觀鎖中的共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程
但是在效率方面,處理加鎖的機制會產(chǎn)生額外的開銷,還有增加產(chǎn)生死鎖的機會。另外還會降低并行性,如果已經(jīng)鎖定了一個線程A,其他線程就必須等待該線程A處理完才可以處理
數(shù)據(jù)庫中的行鎖,表鎖,讀鎖(共享鎖),寫鎖(排他鎖),以及syncronized實現(xiàn)的鎖均為悲觀鎖
悲觀并發(fā)控制實際上是“先取鎖再訪問”的保守策略,為數(shù)據(jù)處理的安全提供了保證
2.2、樂觀鎖
樂觀鎖(Optimistic Lock): 就是很樂觀,每次去拿數(shù)據(jù)的時候都認為別人不會修改。所以不會上鎖,但是如果想要更新數(shù)據(jù),則會在更新前檢查在讀取至更新這段時間別人有沒有修改過這個數(shù)據(jù)。如果修改過,則重新讀取,再次嘗試更新,循環(huán)上述步驟直到更新成功(當(dāng)然也允許更新失敗的線程放棄操作),樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量
相對于悲觀鎖,在對數(shù)據(jù)庫進行處理的時候,樂觀鎖并不會使用數(shù)據(jù)庫提供的鎖機制。一般的實現(xiàn)樂觀鎖的方式就是記錄數(shù)據(jù)版本(version)或者是時間戳來實現(xiàn),不過使用版本記錄是最常用的。
樂觀控制相信事務(wù)之間的數(shù)據(jù)競爭(data race)的概率是比較小的,因此盡可能直接做下去,直到提交的時候才去鎖定,所以不會產(chǎn)生任何鎖和死鎖。
三、鎖的實現(xiàn)
悲觀鎖阻塞事務(wù)、樂觀鎖回滾重試:它們各有優(yōu)缺點,不要認為一種一定好于另一種。像樂觀鎖適用于寫比較少的情況下,即沖突真的很少發(fā)生的時候,這樣可以省去鎖的開銷,加大了系統(tǒng)的整個吞吐量。但如果經(jīng)常產(chǎn)生沖突,上層應(yīng)用會不斷的進行重試,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。
3.1 悲觀鎖的實現(xiàn)方式
場景:
- 有用戶A和用戶B,在同一家店鋪去購買同一個商品,但是商品的可購買數(shù)量只有一個
下面是這個店鋪的商品表t_goods結(jié)構(gòu)和表中的數(shù)據(jù):
在不加鎖的情況下,如果用戶A和用戶B同時下單,就會報錯。
悲觀鎖的實現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機制,在數(shù)據(jù)庫中,我們?nèi)绾斡帽^鎖去解決這個事情呢?
- 加入當(dāng)用戶A對下單購買商品(臭豆腐)的時候,先去嘗試對該數(shù)據(jù)(臭豆腐)加上悲觀鎖
- 加鎖失?。赫f明商品(臭豆腐)正在被其他事務(wù)進行修改,當(dāng)前查詢需要等待或者拋出異常,具體返回的方式需要由開發(fā)者根據(jù)具體情況去定義
- 加鎖成功:對商品(臭豆腐)進行修改,也就是只有用戶A能買,用戶B想買(臭豆腐)就必須一直等待。當(dāng)用戶A買好后,用戶B再想去買(臭豆腐)的時候會發(fā)現(xiàn)數(shù)量已經(jīng)為0,那么B看到后就會放棄購買
- 在此期間如果有其他對該數(shù)據(jù)(臭豆腐)做修改或加鎖的操作,都會等待我們解鎖后或者直接拋出異常
那么如何加上悲觀鎖呢?我們可以通過以下語句給id=2的這行數(shù)據(jù)加上悲觀鎖,首先關(guān)閉MySQL數(shù)據(jù)庫的自動提交屬性。因為MySQL默認使用autocommit模式,也就是說,當(dāng)我們執(zhí)行一個更新操作后,MySQL會立刻將結(jié)果進行提交, (sql語句:setautocommit=0)
悲觀鎖加鎖sql語句:
- select num from t_goods where id = 2 for update
我們通過開啟mysql的兩個會話,也就是兩個命令行來演示:
事務(wù)A:我們可以看到數(shù)據(jù)是立刻馬上就可以查詢出來,num=1
事務(wù)B:我們是可以看到,事務(wù)B會一直等待事務(wù)A釋放鎖。如果事務(wù)A長期不釋放鎖,那么最終事務(wù)B將會報錯,報錯如下圖所示,表示語句已被鎖住。
現(xiàn)在我們讓事務(wù)A執(zhí)行命令去修改數(shù)據(jù),讓臭豆腐的數(shù)量減一,然后查看修改后的數(shù)據(jù),最后commit,結(jié)束事務(wù)
我們可以看到當(dāng)我們事務(wù)A執(zhí)行完成之后,臭豆腐的庫存只有0個了,這個時候我們用戶B再來購買這個臭豆腐的時候就會發(fā)現(xiàn),最后一個臭豆腐已經(jīng)被用戶A購買完了,那么用戶B只能放棄購買臭豆腐了。
通過悲觀鎖我們可以解決因為商品庫存不足,導(dǎo)致的商品超出庫存的售賣。
3.1 樂觀鎖的實現(xiàn)方式
對于上面的應(yīng)用場景,我們應(yīng)該怎么用樂觀鎖去解決呢?在上面的樂觀鎖中,我們有提到使用版本號(version)來解決,所以我們需要在t_goods加上版本號,調(diào)整后的sql表結(jié)構(gòu)如下:
具體操作步驟如下:
1、首先用戶A和用戶B同時將臭豆腐(id=2)的數(shù)據(jù)查出來
2、然后用戶A先買,用戶A將(id=1和version=0)作為條件進行數(shù)據(jù)更新,將數(shù)量-1,并且將版本號+1。此時版本號變?yōu)?。用戶A此時就完成了商品的購買
3、 用戶B開始買,用戶B也將(id=1和version=0)作為條件進行數(shù)據(jù)更新
4、更新完后,發(fā)現(xiàn)更新的數(shù)據(jù)行數(shù)為0,此時就說明已經(jīng)有人改動過數(shù)據(jù),此時就應(yīng)該提示用戶B重新查看最新數(shù)據(jù)購買
1、首先我們開啟兩個會話窗口,輸入查詢語句:selectnumfromt_goodswhere id=2
事務(wù)A:
事務(wù)B:
這個時候事務(wù)A和事務(wù)B同時獲取相同的數(shù)據(jù)
2、此時事務(wù)A進行更新數(shù)據(jù)的操作,然后在查詢更新后的數(shù)據(jù)
這個時候我們可以看到事務(wù)A更新成功,并且?guī)齑?1 版本號+1成功
2、此時事務(wù)B進行更新數(shù)據(jù)的操作,然后在查詢更新后的數(shù)據(jù)
可以看到最終修改的時候失敗,數(shù)據(jù)沒有改變。此時就需要我們告知用戶B重新處理
3.1.1 CAS
說到樂觀鎖,就必須提到一個概念:CAS 什么是CAS呢?Compare-and-Swap,即比較并替換,也有叫做Compare-and-Set的,比較并設(shè)置。1、比較:讀取到了一個值A(chǔ),在將其更新為B之前,檢查原值是否仍為A(未被其他線程改動)。2、設(shè)置:如果是,將A更新為B,結(jié)束。[1]如果不是,則什么都不做。上面的兩步操作是原子性的,可以簡單地理解為瞬間完成,在CPU看來就是一步操作。有了CAS,就可以實現(xiàn)一個樂觀鎖,允許多個線程同時讀取(因為根本沒有加鎖操作),但是只有一個線程可以成功更新數(shù)據(jù),并導(dǎo)致其他要更新數(shù)據(jù)的線程回滾重試。CAS利用CPU指令,從硬件層面保證了操作的原子性,以達到類似于鎖的效果。
Java中真正的CAS操作調(diào)用的native方法因為整個過程中并沒有“加鎖”和“解鎖”操作,因此樂觀鎖策略也被稱為無鎖編程。換句話說,樂觀鎖其實不是“鎖”,它僅僅是一個循環(huán)重試CAS的算法而已!
四、如何選擇
悲觀鎖阻塞事務(wù),樂觀鎖回滾重試,它們各有優(yōu)缺點,不要認為一種一定好于另一種。像樂觀鎖適用于寫比較少的情況下,即沖突真的很少發(fā)生的時候,這樣可以省去鎖的開銷,加大了系統(tǒng)的整個吞吐量。
但如果經(jīng)常產(chǎn)生沖突,上層應(yīng)用會不斷的進行重試,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。
注意點:
1、樂觀鎖并未真正加鎖,所以效率高。一旦鎖的粒度掌握不好,更新失敗的概率就會比較高,容易發(fā)生業(yè)務(wù)失敗。
2、悲觀鎖依賴數(shù)據(jù)庫鎖,效率低。更新失敗的概率比較低。
五、總結(jié)
這篇文章講解了悲觀鎖與樂觀鎖的區(qū)別,以及實現(xiàn)場景,不管是悲觀鎖還是樂觀鎖都是人們定義出來的概念,是一種思想,