序
此篇博客是【眼見為實】系列的第一篇博客,主要從理論上講了數據庫並發可能會出現的問題,解決並發問題的技術——封鎖,封鎖約定的規則——封鎖協議。然后簡單說明了數據庫事務隔離級別和封鎖協議的對應關系。后面的幾篇博客都是通過親身實踐探究InnoDB引擎在各個隔離級別下的實現細節。
【眼見為實】自己動手實踐理解READ UNCOMMITED && SERIALIZABLE
【眼見為實】自己動手實踐理解 READ COMMITTED && MVCC
【眼見為實】自己動手實踐理解REPEATABLE READ && Next-Key Lock
數據庫並發的幾大類問題
①丟失修改(Lost Update)
兩個事務T1和T2同時讀入同一數據並修改,T2的提交的結果破壞了T1提交的結果,導致T1的修改被丟失(第二類丟失更新)。
還有一種特殊的丟失修改(第一類丟失更新),如下圖。因為這種丟失修改在【READ UNCOMMITED】隔離級別下都不會出現,所以不進行討論。
②不可重復讀(Non-Repeatable Read)
事務T1讀取數據后,事務T2執行更新操作,使事務T1無法再現前一次讀取結果。
具體包括三種情況:
(1)事務T1讀取某一數據后,事務T2對其做了修改,當事務T1再次讀取該數據時,得到與前一次不同的值。
(2)事務T1按照一定條件讀取了某些數據記錄后,事務T2刪掉了其中部分記錄,當T1再次按相同條件查詢數據時,發現某些記錄消失了。
(3)事務T1按照一定條件讀取了某些數據記錄后,事務T2插入了一些記錄,當T1再次按相同條件查詢數據時,發現多了一些記錄。
③幻讀(Phantom Read)
幻讀其實是不可重復讀的一種特殊情況。不可重復讀(2)和(3)也稱為幻讀現象。不可重復讀是對數據的修改更新產生的;而幻讀是插入或刪除數據產生的。
④讀臟數據(Dirty Read)
事務T1修改某一數據,並將其寫回磁盤,事務T2讀取同一數據后,T1因為某些原因回滾,這時T1修改過的數據恢復原值,T2讀取到的數據就與數據庫中的數據不一致,則T2讀取到數據就為“臟數據“,即不正確的數據。
並發控制的主要技術是封鎖
基本封鎖類型:
①排它鎖(Exclusive Locks,簡稱X鎖)
排它鎖又稱為寫鎖。若事務T對數據對象A加上X鎖,則只允許T修改和讀取A,其他任何事務都不能再對A加任何類型的鎖,直到T釋放A上的鎖。這就保證了其他事務在T釋放A上的鎖之前都不能再讀取和修改A。
②共享鎖(Share Locks,簡稱S鎖)
共享鎖又稱為讀鎖。若事務T對數據對象A加上S鎖,則事務T可以讀取A但不能修改A。其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這就保證了其他事務可以讀取A,但是在T釋放A上的S鎖之前不能對A做任何修改。
排它鎖與共享鎖的相容矩陣
封鎖協議
在運用X鎖和S鎖這兩種基本封鎖,對數據對象加鎖時,還需要約定一些規則。例如何時申請X鎖和S鎖,持鎖時間,何時釋放等。這些規格稱為封鎖協議。
一級封鎖協議
一級封鎖協議:事務T在修改數據A之前必須對其加X鎖,直到事務結束才釋放。事務結束包括正常結束(Commit)和非正常結束(RollBack)。
一級封鎖協議可防止丟失修改。
使用一級封鎖協議解決了圖1中的覆蓋丟失問題。事務T1在讀A進行修改之前先對A加X鎖,當T2再請求對A加X鎖時被拒絕,T2只能等待T1釋放A上的鎖后T2獲得A上的X鎖,這時它讀取的A已經是T1修改后的15,再按照此值進行計算,將結果值A=14寫入磁盤。這樣就避免了丟失T1的更新。
二級封鎖協議
二級封鎖協議:一級封鎖協議加上事務T在讀取數據A之前必須先對其加S鎖,讀完后即可釋放S鎖。
二級封鎖協議除防止了丟失修改,還進一步防止了讀“臟”數據。
使用二級封鎖協議解決了圖2中的臟讀問題。事務T1在讀C進行修改之前先對C加X鎖,修改其值后寫回磁盤。這時T2請求在C上加S鎖,因為T1在C上已經加了X鎖,所以T2只能等待。T1因為某種原因被撤銷,C恢復原值100。T1釋放C上的X鎖后T2獲得C上的S鎖,讀C=100。這樣就避免了讀“臟”數據。
三級封鎖協議
三級封鎖協議:一級封鎖協議加上事務T在讀取數據A之前必須先對其加S鎖,直到事務結束才釋放。
三級封鎖協議除防止了丟失修改和讀“臟”數據,還進一步防止了不可重復讀。
使用三級封鎖協議解決了圖3中的不可重復讀問題。事務T1在讀取數據A和數據B之前對其加S鎖,其他事務只能再對A、B加S鎖,不能加X鎖,這樣其他事務只能讀取A、B,而不能更改A、B。這時T2請求在B上加X鎖,因為T1已經在B上加了S鎖,所以T2只能等待。T1為了驗算結果再次讀取A、B的值,因為其他事務無法修改A、B的值,所以結果仍然為150,即可重復讀。此時T1釋放A、B上的S鎖,T2才獲得B上的X鎖。這樣就避免了不可重復讀。
活鎖和死鎖
封鎖可能會引起活鎖活死鎖。
活鎖
如果事務T1封鎖了數據R,事務T2又請求封鎖數據R,於是T2等待。事務T3也請求封鎖R,當事務T1釋放了數據R上的封鎖之后系統首先批准了事務T3的封鎖請求,T2仍然等待。然后T4又申請封鎖R,當T3釋放了R的封鎖之后系統又批准了T4的封鎖請求。T2有可能一直等待下去,這就是活鎖。
避免活鎖的方法就是先來先服務的策略。當多個事務請求對同一數據對象封鎖時,封鎖子系統按照請求的先后對事務排隊。數據對象上的鎖一旦釋放就批准申請隊列中的第一個事務獲得鎖。
死鎖
如果事務T1封鎖了數據R1,事務T2封鎖了數據R2,然后T1又請求封鎖數據R2,因為T2已經封鎖了數據R2,於是T1等待T2釋放R2上的鎖。接着T2又申請封鎖R1,因為因為T1已經封鎖了數據R1,T2也只能等待T1釋放R1上的鎖。這樣就出現了T1在等待T2,T2也在等待T1的局面,T1和T2兩個事務永遠不能結束,形成死鎖。
死鎖的預防:
①一次封鎖法
一次封鎖法要求事務必須一次將所有要使用的數據全部加鎖,否則不能繼續執行。例如上圖中的事務T1將數據R1和R2一次加鎖,T1就能執行下去,而T2等待。T1執行完成之后釋放R1,R2上的鎖,T2繼續執行。這樣就不會產生死鎖。
一次封鎖法雖然能防止死鎖的發生,但是缺點卻很明顯。一次性將以后要用到的數據加鎖,勢必擴大了封鎖的范圍 ,從而降低了系統的並發度。
②順序封鎖法
順序封鎖法是預先對數據對象規定一個封鎖順序,所有的事務都按照這個順序實行封鎖。
順序封鎖法雖然可以有效避免死鎖,但是問題也很明顯。第一,數據庫系統封鎖的數據對象極多,並且隨着數據的插入、刪除等操作不斷變化,要維護這樣的資源的封鎖順序非常困難,成本很高。第二,事務的封鎖請求可以隨着事務的執行動態的確定,因此很難按照規定的順序實行封鎖。
可見,預防死鎖的產生並不是很適合數據庫的特點,所以在解決死鎖的問題上普遍采用的是診斷並且解除死鎖。
死鎖的診斷與解除:
①超時法
如果一個事務的等待時間超過了默認的時間,就認為是產生了死鎖。
②等待圖法
一旦檢測到系統中存在死鎖就要設法解除。通常的解決方法是選擇一個處理死鎖代價最小的事務,將其撤銷,釋放此事務持有的所有的鎖,恢復其所執行的數據修改操作,使得其他事務得以運行下去。
兩段鎖協議
所謂的二段鎖協議是指所有事務必須分兩個階段對數據進行加鎖和解鎖操作。
-
在對任何數據進行讀、寫操作之前,首先要申請並獲得該數據的封鎖。
-
在釋放一個封鎖之后,事務不在申請和獲得其他封鎖。
也就是說事務分為兩個階段。第一個階段是獲得封鎖,也稱為擴展階段。在這個階段,事務可以申請獲得任何數據項任何類型的鎖,但是不能釋放任何鎖。第二階段是釋放封鎖,也稱為收縮階段。在這個階段,事務可以釋放任何數據項上任何類型的封鎖,但是不能再申請任何鎖。
事務遵守兩段鎖協議是可串行化調度的充分條件,而不是必要條件。也就是說遵守兩段鎖協議一定是可串行化調度的,而可串行化調度的不一定是遵守兩段鎖協議的。
左側T1、T2遵循兩段鎖協議,右側T1、T2並不遵循兩段鎖協議
兩段鎖協議和一次封鎖法的異同
一次封鎖法要求事務必須將要使用的數據全部加鎖,否則不能繼續執行。因此一次封鎖法遵守兩段鎖協議。
但是兩段鎖協議並不要求事務將要使用的數據一次全部加鎖,因此兩段鎖協議可能發生死鎖。如圖:
數據庫隔離級別
封鎖協議和隔離級別並不是嚴格對應的。
各種隔離級別所能避免的並發問題
本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。
如果您認為還不錯,不妨點擊一下下方的[【推薦】](javascript:void(0)😉按鈕,謝謝支持。
轉載與引用請注明出處。