資料庫是一個多使用者使用的共用資源,當多個使用者並發地存取資料時,在資料庫中就會產生多個事務同時存取同一資料的情況。若對並行作業不加控制就可能會讀取和儲存不正確的資料,破壞資料庫的一致性。加鎖是實現資料庫並發控制的一個非常重要的技術。在實際應用中經常會遇到的與鎖相關的異常情況,當兩個事務需要一組有衝突的鎖,而不能將事務繼續下去的話,就會出現死結,嚴重影響應用的正常執行。
在資料庫中有兩種基本的鎖類型:排它鎖(Exclusive Locks,即X鎖)和共用鎖定(Share Locks,即S鎖)。當資料對象被加上排它鎖時,其他的事務不能對它讀取和修改。加了共用鎖定的資料對象可以被其他事務讀取,但不能修改。資料庫利用這兩種基本的鎖類型來對資料庫的事務進行並發控制。
下面總結下這兩種鎖造成的常見的死結情況與解決方案: 一. 事務之間對資源訪問順序的交替 出現原因:
一個使用者A 訪問表A(鎖住了表A),然後又訪問表B;另一個使用者B 訪問表B(鎖住了表B),然後企圖訪問表A;這時使用者A由於使用者B已經鎖住表B,它必須等待使用者B釋放表B才能繼續,同樣使用者B要等使用者A釋放表A才能繼續,這就死結就產生了。 解決方案:
這種死結比較常見,是由於程式的BUG產生的,除了調整的程式的邏輯沒有其它的辦法。仔細剖析器的邏輯,對於資料庫的多表操作時,盡量按照相同的順序進行處理,盡量避免同時鎖定兩個資源,如操作A和B兩張表時,總是按先A後B的順序處理, 必須同時鎖定兩個資源時,要保證在任何時刻都應該按照相同的順序來鎖定資源。 二. 並發修改同一記錄 出現原因:
使用者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 transelect * from table(updlock) (加更新鎖定)update table set column1='hello'T2:begin transelect * from table(updlock)update table set column1='world'
更新鎖定的意思是:“我現在只想讀,你們別人也可以讀,但我將來可能會做更新操作,我已經擷取了從共用鎖定(用來讀)到獨佔鎖定(用來更新)的資格”。一個事物只能有一個更新鎖定獲此資格。
T1執行select,加更新鎖定。
T2運行,準備加更新鎖定,但發現已經有一個更新鎖定在那兒了,只好等。
當後來有user3、user4…需要查詢table表中的資料時,並不會因為T1的select在執行就被阻塞,照樣能查詢,提高了效率。 三. 索引不當導致全表掃描 出現原因:
如果在事務中執行了一條不滿足條件的語句,執行全表掃描,把行級鎖上升為表級鎖,多個這樣的事務執行後,就很容易產生死結和阻塞。類似的情況還有當表中的資料量非常龐大而索引建的過少或不合適的時候,使得經常發生全表掃描,最終應用系統會越來越慢,最終發生阻塞或死結。 解決方案:
SQL語句中不要使用太複雜的關聯多表的查詢;使用“執行計畫”對SQL語句進行分析,對於有全表掃描的SQL語句,建立相應的索引進行最佳化。 四.事務封鎖範圍大且相互等待
可參考我這篇文章