數據庫死鎖的解決辦法


近日在博客網站上,回復別人的數據庫死鎖避免問題,之前也曾經幾次答復過同樣的內容,覺得很有必要匯聚成一個博客文章,方便大家。

這里的辦法,對所有的數據庫都適用。

首先說明:數據庫的死鎖問題,通過巧妙的設計,死鎖是可以避免的。

 

這個解決辦法步驟如下:

1. 每個表中加 updated_count (integer) 字段

 

2. 新增一行數據時, 填入值 updated_count =0 :
insert into table_x (f1,f2,...,update_count) values(...,0);

 

3. 根據主鍵獲取一行數據 SQL,封裝成一個 DAO 函數(我的習慣是每個表一個 uuid 字段做主鍵。從不用組合主鍵,組合主鍵在多表 join 時 SQL 寫起來很麻煩;也不用用戶錄入的業務數據做主鍵,因為凡是用戶錄入的數據都可能錯誤,然后要更改,不適合做主鍵)。
select * from table_x where pk = ?

 

4. 刪除一行數據時
4.1 先通過主鍵獲取此行數據, 見 3.

4.2 delete from table_x where pk = ? and update_count=? , 這里 where 中的 update_count 數值通過 4.1 中獲取
4.3 檢查 4.2 執行影響數據行數,如果刪除失敗,則是別人已經刪除或者更新過同一行數據,拋異常,在最外面 rollback,並通過合適的詞語提醒用戶有並發操作,請稍候再試。
int count = cmd.ExecuteNonQuery();
if(udpatedCount < 1){
throw new Exception(“檢測到並發操作,為防止死鎖,已放棄當前操作,請稍候再試,表 xxx, 數據 key ….”);
}

 

5. 更新一行數據時
5.1 先通過主鍵獲取此行數據, 見 3.
5.2 update table_x set f1=?,f2=?, ...,update_count=update_count+1 where pk = ? and update_count=? , 這里where 中的 update_count 數值通過 5.1 中獲取
5.3 檢查 5.2 執行影響數據行數,如果更新失敗,則是別人已經刪除或者更新過同一行數據,拋異常,在最外面 rollback,並通過合適的詞語提醒用戶有並發操作,請稍候再試。
int count = cmd.ExecuteNonQuery();
if(udpatedCount < 1){
throw new Exception(“檢測到並發操作,為防止死鎖,已放棄當前操作,請稍候再試,表 xxx, 數據 key ….”);
}

 

6. 數據庫訪問層 DAO 中,絕對不要寫 try catch,也不要寫 commit/rollback.

因為當我寫了一個 dao1.insert(xxx) ,另一個人寫了 dao2.insert(xxx), 兩周后有可能會有人把這兩個函數組合在一起放在一個事務中。如果dao1.insert(xxx)已經 commit ,那么dao2.insert(xxx) 中rollback 會達不到期望效果。很多電腦書中示例代碼,都有這個錯誤。


數據庫事務應該是這樣界定起始范圍:

6.1 單機版程序,每個按鈕操作,對應一個事務。

可以在把 connection/transaction 傳遞到 dao 中。在按鈕響應的代碼處,處理事務。catch 到任何 Exception 都要 rollback.


6.2 網頁版程序,每個按鈕操作,對應一個事務。

可以在把 connection/transaction 傳遞到 dao 中。在按鈕響應的代碼處,處理事務。我強烈建議對於 Web應用,數據庫連接的打開/關閉、數據庫事務的開始和 commit/rollback 全在 filter 中處理(Java EE 和 ASP.NET MVC 都有 filter, 其它的不知道),事務、數據庫連接通過 threadlocal 傳入到 DAO 中。filter 中 catch 到任何 Exception 都要 rollback.

 

見過很多用 Spring 的人,代碼中啟動了幾個數據庫事務自己都不知道,符不符合自己的需要,也不知道。我的建議是,禁止使用 Spring 管理數據庫事務

 

7. 單表的增、刪、改、通過主鍵查,應該用工具自動生成。

自動生成代碼,應該放在單獨一個目錄,以便后面有數據庫表改動,可以重新生成代碼並覆蓋。自動生成的文件,在第一行就寫上注釋,表示這是一個自動生成的文件,以后會被自動覆蓋,所以不要改這個文件。

舉例來說,對於 tm_system_user 表,可以自動生成 TmSystemUserDAO, 包含函數: insert(TmSystemUser), update(TmSystemUser), delete(TmSystemUser), getByKey(key), batchInsert(TmSystemUser[])。


8. 總是使用事務,並用 ReadCommited 級別。

即使是純查詢 SQL,也這么寫(總是使用事務,並用 ReadCommited 級別)。這可以簡化設計與寫代碼,沒有發現明顯多余的性能消耗。

 

9. 數據設計時,盡量避免后續代碼中可能出現的 update/delete 操作。

舉例來說,如果是一個請假條的審批流程,把請假條申請設計成一個表,領導批復設計成另一個表。盡量避免:設計時合並成一個表,把批准狀態(同意/否決)、批准時間當成“請假條申請”的屬性

此處申請數據應設計成一個表,批復數據應設計成另一個表。
說極端一點,最好從數據庫設計上,避免后續編程有 update/delete, 只有 insert。 好像現在流行的 NoSQL 也是這么個思路。

 

10. 補充,

如果在后台檢查頁面錄入數據,報錯處理,有以下兩種方法:

10.1 只要有一個錯誤,就 throw exception.

10.2 把所有的錯誤都檢測出來,比如,用戶名未錄入,電子郵件未錄入,放在一個 List中,然后 throw exception.

-------------------------------------------------- 

2012-3-30, 由於很多網友對數據庫死鎖了解不深,甚至有部分網友,不知道數據庫會死鎖僵住,特補充一些資料。以下內容,節選自《LINQ實戰》:
8.1.1 悲觀式並發

在.NET出現之前,很多應用程序都需要自行管理與數據庫之間的連接。在這些系統中,開發人員經常在獲取某條記錄之后為其加鎖,用來阻止其他用戶可能在同時作出的修改。此類加鎖的策略就叫做悲觀式並發。悲觀式並發對於某些小型的Windows桌面程序來講可能沒有什么問題,不過若是在用戶很多的大型系統中使用同樣的策略,那么系統的整體性能很快就會被拖累下來。

隨着系統規模的擴大,可伸縮性問題開始浮出水面。因此,很多系統從客戶端-服務器架構遷移到了更少狀態信息的、基於Web的應用程序,這也同時降低了部署的成本。無狀態的Web應用程序也讓過於保守的悲觀式並發策略再無用武之地。

為了讓開發者避免陷入到悲觀式並發所帶來的可伸縮性以及加鎖的泥沼中,.NET Framework在設計之初就考慮到了Web應用程序的離線特性。.NET以及ADO.NET所提供的API均無法鎖住某張數據表,這樣自然就終結了悲觀式並發的可能。不過如果需要的話,應用程序同樣能在第一次獲取某條記錄的同時為其添加一個"簽出"標簽,這樣在第二次嘗試訪問時,即可獲得該"簽出"情況,並根據需要進行相應的處理。不過很多情況下,由於很難確定用戶是否不再使用這個標簽,因此"簽出"標簽會經常處於未重新設置狀態。正因為這樣,悲觀式並發在離線程序中的使用頻率也越來越低。

8.1.2 樂觀式並發

由於離線環境下的程序常常不適合使用悲觀式並發,因此另一種處理的策略,即樂觀式並發逐漸出現在人們的視線中。樂觀式並發允許任意多的用戶隨時修改他們自己的一份數據的拷貝。在提交修改時,程序將檢查以前的數據是否有所改變。若沒有變化,則程序只需保存修改即可。若發生了變化並存在沖突,那么程序將根據實際情況決定是將前一修改覆蓋掉,還是把這一次新的修改丟棄,或是嘗試合並兩次修改。

樂觀式並發的前一半操作相對來說比較簡單。在不需要並發檢查的情況下,數據庫中使用的SQL語句將類似於如下語法:UPDATE TABLE SET [field = value] WHERE [Id = value]不過在樂觀式並發中,WHERE子句將不只包含ID列,同時還要比較表中其他各列是否與原有值相同。

在代碼清單8-1中,我們在最后通過檢查RowCount來查看這次更新是否成功。若RowCount為1,則表明原有記錄在該用戶修改的期間並沒有被別人更新,即更新成功。若RowCount為0,則意味着有人在期間修改了該記錄。此時該記錄將不會被更新,程序也能夠告知用戶有關該沖突的信息,並根據需要執行合適的操作...

-------------------------------------------------- 

2012-3-31 補充:

Oracle中的TimeStamp(時間戳)與SqlServer中的差別很大。SqlServer中的TimeStamp是二進制格式存儲在數據庫中,可以將DataSet中的這個字段類型設定為base64Binary類型。Oracle中的TimeStamp是時間格式存儲的。

SQL Server 有個函數名叫 CURRENT_TIMESTAMP,與 SqlServer中的TimeStamp 數據列類型,沒有一毛錢的關系。

個人認為 SqlServer中的TimeStamp 數據列類型,屬於“名詞亂用”,與一般人理解中的 timestamp 不是一個意思。繼續從互聯網上查找,果然有發現: 

Transact-SQL timestamp 數據類型與在 SQL-92 標准中定義的 timestamp 數據類型不同。SQL-92 timestamp 數據類型等價於 Transact-SQL datetime 數據類型。
 Microsoft SQL Server 將來的版本可能會修改 Transact-SQL timestamp 數據類型的行為,使它與在標准中定義的行為一致。到那時,當前的 timestamp 數據類型將用 rowversion 數據類型替換。
 Microsoft SQL Server 2000 引入了 timestamp 數據類型的 rowversion 同義詞。在 DDL 語句中盡可能使用 rowversion 而不使用 timestamp。rowversion 受數據類型同義詞行為的制約。

沒有看出 SQL Server timestamp 和數據庫死鎖有何關系!!!即使是微軟 LINQ for SQL 自動生成的代碼,也是沒有用到 timestamp ,而是用了本博客文章中的技術。 如有哪位網友提供資料,說明 SQL Server timestamp 和數據庫死鎖有點關系,將不勝感謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM