數據庫(六),鎖
在數據庫(五),事務裏面我們講了事務ACID屬性,事務最重要的能在異常情況的修復以及並發連接的處理上。
異常情況的修復主要通過日誌來完成,那麽並發連接的處理主要通過鎖。本章主要整理的是鎖的相關知識。
為什麽需要鎖?
現在Bob的賬戶裏面有1000塊錢,此時程序突然同時來了兩個要求,一個要把Bob的錢轉給Smith 20塊,一個要把Bob的錢轉Joe 30塊。這兩個要求一查Bob的賬戶,都發現現在Bob有1000塊,所以要求A算出現在Bob應該有980塊,要求B算出來Bob應有970。要求A的數據被要求B的數據覆蓋了。
這樣就出問題了,明明應該扣50塊錢,現在卻只是扣了30塊。
鎖就是用來解決這樣的並發訪問的問題。當每次訪問Bob賬戶之前,都加一個鎖,禁止別人再次訪問,只有等待持有鎖的人來釋放
悲觀鎖和樂觀鎖
悲觀鎖
如果事務A把Bob賬戶鎖住了,事務B自然不能操作Bob賬戶,也就是說其他線程只能在外面等待。
這種加鎖的方式就是悲觀鎖。它每次取讀寫數據時總認為數據會被別人修改,所以將數據加鎖,置於鎖定狀態,不讓別人訪問。
缺點是如果持有鎖的時間太長,其他用戶需要等待很長的時間。
悲觀鎖主要適用於並發爭搶比較嚴重的場景。
樂觀鎖
悲觀鎖的問題顯而易見,如果將數據加鎖了以後,其他的線程是無法訪問的,只能等待。如果持有鎖的時間太長,需要等待大量的時間。
所以我們引入了樂觀鎖,所謂樂觀鎖是認為一般情況下不會有太多的人修改余額,所有沒有加鎖,只有在最後更新的時候才去看是否有沖突。
那具體怎麽做呢?
可以在日誌中加上一個version(版本)字段,
每次讀的時候,不僅需要讀出余額,還需要讀出版本號。
- 等修改了余額以後,往回寫之前需要檢查一下版本號,看看與讀的時候版本號是否一樣。
如果不一樣,說明數據已經被改變了,所以需要放棄寫操作,重新讀取余額和版本號
如果一樣,則將新余額寫回去,把版本號加1 。
比如
事務1把Bob的余額減去30,此時它讀到了(Bob余額=1000,版本=1)
事務2也需要將Bob的余額減去50,他也讀到了(Bob余額=1000,版本=1)
然後事務1率先完成計算,把新的余額值970寫回了,版本 加 1 ,變成了版本2。
事務2寫回去的時候,發現最新的版本號變為2,表示之前讀的數據已經改變,所以需要重新讀一遍
這就是樂觀鎖,這種方式適合於沖突不多的場景,如果沖突很多,數據爭用激烈,會導致不斷的嘗試,反而降低了性能。
死鎖
死鎖產生的條件
如果出現如下這種情況
有兩個線程同時參與
這兩個線程在不同方向給同一個資源加鎖
爭搶相同的資源
那麽很可能出現死鎖
比如事務1是Bob給Smith轉賬,事務2是Smith給Bob轉賬。
當這兩個事務單元同時發生的時候,就有問題呢。
事務單元1會先鎖定Bob,然後鎖定Smith,而事務單元2會先鎖定Smith,然後鎖定Bob
事務1會等待事務2把Bob給釋放了,而事務2在等待事務1把Smith釋放了。
如何解決
那麽如何解決死鎖呢?最好的方法是盡可能不出現死鎖,當然很難。或者說如果鎖定時間超時了,則強行釋放,不過這種方法效率比較低,因為如果有用戶的事務本來時間就很長,則每個死鎖的檢測時間將會很長。
所以最優的方案在於預測死鎖,可以把事務單元等待的鎖記錄下來
比如下圖中,事務單元1持有"Lock Bob"的鎖,現在又在申請一把"Lock Smith"的鎖,在申請之前,可以查看同樣申請了"Lock Smith"的有哪些事務單元。明顯事務單元2也申請過這把鎖。好了,下一步是看事務單元2在申請什麽鎖呢,發現它居然在申請"Lock Bob"這把鎖,而這把鎖目前由事務單元1持有。所以現在已經發現有死鎖的可能了,也就是發生了碰撞。所以可以提前補救。
U鎖
下面來討論一種死鎖的情況。如下圖
事務1 Trx1
開始事務1
讀A(讀鎖)
A - 100(讀鎖需要升級為寫鎖)
提交事務1
事務2 Trx2
開始事務2
讀A(讀鎖)
A - 100(讀鎖需要升級為寫鎖)
提交事務2(解鎖)
事務1和事務2的讀鎖是可以並行的,所以讀鎖可以同時進入臨界區,但是寫鎖不能,會被擋在外面。此時事務2又發起了寫鎖。那麽尷尬的局面就產生了。
事務1的寫鎖需要等事務2的讀鎖釋放資源。
事務2的寫鎖需要等待事務1的讀鎖釋放資源。
所以形成了死鎖。其實這種死鎖的形成條件非常的簡單,只需要針對同一個數據進行讀寫。比如說update set A=A-1 where id = 100
如果運行多次,就會出現死鎖
解決的辦法是引入U鎖,可以將讀鎖直接升級為寫鎖。
對於事務1,讀以後馬上就是寫,所以直接就使用寫鎖,而不是讀鎖呢。
同理事務2也是如此。
數據庫(六),鎖