數據庫系列課程(20)-數據庫死鎖的原因及解決方案

數據庫是一個多用戶使用的共享資源,當多個用戶併發地存取數據時,在數據庫中就會產生多個事務同時存取同一數據的情況。若對併發操作不加控制就可能會讀取和存儲不正確的數據,破壞數據庫的一致性。加鎖是實現數據庫併發控制的一個非常重要的技術。在實際應用中經常會遇到的與鎖相關的異常情況,當兩個事務需要一組有衝突的鎖,而不能將事務繼續下去的話,就會出現死鎖,嚴重影響應用的正常執行。

在數據庫中有兩種基本的鎖類型:排它鎖(Exclusive Locks,即X鎖)和共享鎖(Share Locks,即S鎖)。

  • 當數據對象被加上排它鎖時,其他的事務不能對它讀取和修改
  • 加了共享鎖的數據對象可以被其他事務讀取,但不能修改。數據庫利用這兩種基本的鎖類型來對數據庫的事務進行併發控制。

下面總結下這兩種鎖造成的常見的死鎖情況與解決方案。

01 事務之間對資源訪問順序的交替

出現原因:

  • 一個用戶A 訪問表A(鎖住了表A),然後又訪問表B;另一個用戶B 訪問表B(鎖住了表B),然後企圖訪問表A;這時用戶A由於用戶B已經鎖住表B,它必須等待用戶B釋放表B才能繼續,同樣用戶B要等用戶A釋放表A才能繼續,這就死鎖就產生了。

解決方法:

  • 這種死鎖比較常見,是由於程序的BUG產生的,除了調整的程序的邏輯沒有其它的辦法。仔細分析程序的邏輯,對於數據庫的多表操作時,儘量按照相同的順序進行處理,儘量避免同時鎖定兩個資源,如操作A和B兩張表時,總是按先A後B的順序處理, 必須同時鎖定兩個資源時,要保證在任何時刻都應該按照相同的順序來鎖定資源。

02 併發修改同一記錄

出現原因:

  • 用戶A查詢一條紀錄,然後修改該條紀錄;這時用戶B修改該條紀錄,這時用戶A的事務裏鎖的性質由查詢的共享鎖企圖上升到獨佔鎖,而用戶B裏的獨佔鎖由於A有共享鎖存在所以必須等A釋放掉共享鎖,而A由於B的獨佔鎖而無法上升的獨佔鎖也就不可能釋放共享鎖,於是出現了死鎖。這種死鎖由於比較隱蔽,但在稍大點的項目中經常發生。

  • 一般更新模式由一個事務組成,此事務讀取記錄,獲取資源(頁或行)的共享 (S) 鎖,然後修改行,此操作要求鎖轉換爲排它 (X) 鎖。如果兩個事務獲得了資源上的共享模式鎖,然後試圖同時更新數據,則一個事務嘗試將鎖轉換爲排它 (X) 鎖。共享模式到排它鎖的轉換必須等待一段時間,因爲一個事務的排它鎖與其它事務的共享模式鎖不兼容;發生鎖等待。第二個事務試圖獲取排它 (X) 鎖以進行更新。由於兩個事務都要轉換爲排它 (X) 鎖,並且每個事務都等待另一個事務釋放共享模式鎖,因此發生死鎖。

解決方法:

  • a. 使用樂觀鎖進行控制。樂觀鎖大多是基於數據版本(Version)記錄機制實現。即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個“version”字段來實現。讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認爲是過期數據。樂觀鎖機制避免了長事務中的數據庫加鎖開銷(用戶A和用戶B操作過程中,都沒有對數據庫數據加鎖),大大提升了大併發量下的系統整體性能表現。Hibernate 在其數據訪問引擎中內置了樂觀鎖實現。需要注意的是,由於樂觀鎖機制是在我們的系統中實現,來自外部系統的用戶更新操作不受我們系統的控制,因此可能會造成髒數據被更新到數據庫中。

  • b. 使用悲觀鎖進行控制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,如Oracle的Select … for update語句,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。如一個金融系統,當某個操作員讀取用戶的數據,並在讀出的用戶數據的基礎上進行修改時(如更改用戶賬戶餘額),如果採用悲觀鎖機制,也就意味着整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,可以想見,如果面對成百上千個併發,這樣的情況將導致災難性的後果。所以,採用悲觀鎖進行控制時一定要考慮清楚。

c. SqlServer可支持更新鎖

爲解決死鎖,SqlServer引入更新鎖,它有如下特徵:

  1. 加鎖的條件:當一個事務執行update語句時,數據庫系統會先爲事務分配一把更新鎖。
  2. 解鎖的條件:當讀取數據完畢,執行更新操作時,會把更新鎖升級爲獨佔鎖。
  3. 與其他鎖的兼容性:更新鎖與共享鎖是兼容的,也就是說,一個資源可以同時放置更新鎖和共享鎖,但是最多放置一把更新鎖。這樣,當多個事務更新相同的數據時,只有一個事務能獲得更新鎖,然後再把更新鎖升級爲獨佔鎖,其他事務必須等到前一個事務結束後,才能獲取得更新鎖,這就避免了死鎖。
  4. 併發性能:允許多個事務同時讀鎖定的資源,但不允許其他事務修改它。
    例子如下:
T1:
begin tran
select * from table(updlock) (加更新鎖)
update table set column1='hello'

T2:
begin tran
select * from table(updlock)
update table set column1='world'

更新鎖的意思是:“我現在只想讀,你們別人也可以讀,但我將來可能會做更新操作,我已經獲取了從共享鎖(用來讀)到排他鎖(用來更新)的資格”。一個事物只能有一個更新鎖獲此資格。
T1執行select,加更新鎖。
T2運行,準備加更新鎖,但發現已經有一個更新鎖在那兒了,只好等。
當後來有user3、user4…需要查詢table表中的數據時,並不會因爲T1的select在執行就被阻塞,照樣能查詢,提高了效率。

03 索引不當導致全表掃描

出現原因:

  • 如果在事務中執行了一條不滿足條件的語句,執行全表掃描,把行級鎖上升爲表級鎖,多個這樣的事務執行後,就很容易產生死鎖和阻塞。類似的情況還有當表中的數據量非常龐大而索引建的過少或不合適的時候,使得經常發生全表掃描,最終應用系統會越來越慢,最終發生阻塞或死鎖。

解決方法:

  • SQL語句中不要使用太複雜的關聯多表的查詢;使用“執行計劃”對SQL語句進行分析,對於有全表掃描的SQL語句,建立相應的索引進行優化。

04 事務封鎖範圍大且相互等待

https://blog.csdn.net/qq_16681169/article/details/73359670

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章