SQL Server數據庫查詢操作時的死鎖總結
當前位置:點晴教程→知識管理交流
→『 技術文檔交流 』
1. 死鎖原理 根據操作系統中的定義:死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處于的一種永久等待狀態。 死鎖的四個必要條件: 對應到SQL Server中,當在兩個或多個任務中,如果每個任務鎖定了其他任務試圖鎖定的資源,此時會造成這些任務永久阻塞,從而出現死鎖;這些資源可能是:單行(RID,堆中的單行)、索引中的鍵(KEY,行鎖)、頁(PAG,8KB)、區結構(EXT,連續的8頁)、堆或B樹(HOBT) 、表(TAB,包括數據和索引)、文件(File,數據庫文件)、應用程序專用資源(APP)、元數據(METADATA)、分配單元(Allocation_Unit)、整個數據庫(DB)。一個死鎖示例如下圖所示:
說明:T1、T2表示兩個任務;R1和R2表示兩個資源;由資源指向任務的箭頭(如R1->T1,R2->T2)表示該資源被改任務所持有;由任務指向資源的箭頭(如T1->S2,T2->S1)表示該任務正在請求對應目標資源;
2. 死鎖排查 (1). 使用SQL Server的系統存儲過程sp_who和sp_lock,可以查看當前數據庫中的鎖情況;進而根據objectID(@objID)(SQL Server 2005)/ object_name(@objID)(Sql Server 2000)可以查看哪個資源被鎖,用dbcc ld(@blk),可以查看最后一條發生給SQL Server的Sql語句; create Table #Who(spid int, ecid int, status nvarchar(50), loginname nvarchar(50), hostname nvarchar(50), blk int, dbname nvarchar(50), cmd nvarchar(50), request_ID int );
dpid int, objid int, indld int, [Type] nvarchar(20), Resource nvarchar(50), Mode nvarchar(10), Status nvarchar(10) );
exec sp_who active --看哪個引起的阻塞,blk insert INTO #Lock exec sp_lock --看鎖住了那個資源id,objid SET @DBName='NameOfDataBase'
select #Lock.* from #Lock JOIN #Who ON #Who.spid=#Lock.spid AND dbname=@DBName;
DECLARE crsr Cursor FOR select blk from #Who where dbname=@DBName AND blk<>0; DECLARE @blk int; open crsr; FETCH NEXT from crsr INTO @blk; WHILE (@@FETCH_STATUS = 0)BEGIN; dbcc inputbuffer(@blk); FETCH NEXT from crsr INTO @blk; END; close crsr; DEALLOCATE crsr;
select #Who.spid,hostname,objid,[type],mode,object_name(objid) as objName from #Lock JOIN #Who ON #Who.spid=#Lock.spid AND dbname=@DBName where objid<>0;
drop Table #Lock;
3. 避免死鎖 上面1中列出了死鎖的四個必要條件,我們只要想辦法破其中的任意一個或多個條件,就可以避免死鎖發生,一般有以下幾種方法(from Sql Server 2005聯機叢書):
4. 死鎖處理方法: (1). 根據2中提供的sql,查看那個spid處于wait狀態,然后用kill spid來干掉(即破壞死鎖的第四個必要條件:循環等待);當然這只是一種臨時解決方案,我們總不能在遇到死鎖就在用戶的生產環境上排查死鎖、Kill sp,我們應該考慮如何去避免死鎖。 (2). 使用SET LOCK_TIMEOUT timeout_period(單位為毫秒)來設定鎖請求超時。默認情況下,數據庫沒有超時期限(timeout_period值為-1,可以用select @@LOCK_TIMEOUT來查看該值,即無限期等待)。當請求鎖超過timeout_period時,將返回錯誤。timeout_period值為0時表示根本不等待,一遇到鎖就返回消息。設置鎖請求超時,破環了死鎖的第二個必要條件(請求與保持條件)。 服務器: 消息 1222,級別 16,狀態 50,行 1 已超過了鎖請求超時時段。 (3). SQL Server內部有一個鎖監視器線程執行死鎖檢查,鎖監視器對特定線程啟動死鎖搜索時,會標識線程正在等待的資源;然后查找特定資源的所有者,并遞歸地繼續執行對那些線程的死鎖搜索,直到找到一個構成死鎖條件的循環。檢測到死鎖后,數據庫引擎 選擇運行回滾開銷最小的事務的會話作為死鎖犧牲品,返回1205 錯誤,回滾死鎖犧牲品的事務并釋放該事務持有的所有鎖,使其他線程的事務可以請求資源并繼續運行
5. 兩個死鎖示例及解決方法 5.1 SQL死鎖 (1). 測試用的基礎數據: create TABLE Lock1(C1 int default(0)); create TABLE Lock2(C1 int default(0)); insert INTO Lock1 VALUES(1); insert INTO Lock2 VALUES(1); (2). 開兩個查詢窗口,分別執行下面兩段sql --Query 1 Begin Tran update Lock1 Set C1=C1+1; WaitFor Delay '00:01:00'; select * from Lock2 Rollback Tran;
--Query 2 Begin Tran update Lock2 Set C1=C1+1; WaitFor Delay '00:01:00'; select * from Lock1 Rollback Tran; 上面的SQL中有一句WaitFor Delay '00:01:00',用于等待1分鐘,以方便查看鎖的情況。 (3). 查看鎖情況 在執行上面的WaitFor語句期間,執行第二節中提供的語句來查看鎖信息:
Query1中,持有Lock1中第一行(表中只有一行數據)的行排他鎖(RID:X),并持有該行所在頁的意向更新鎖(PAG:IX)、該表的意向更新鎖(TAB:IX);Query2中,持有Lock2中第一行(表中只有一行數據)的行排他鎖(RID:X),并持有該行所在頁的意向更新鎖(PAG:IX)、該表的意向更新鎖(TAB:IX); 執行完Waitfor,Query1查詢Lock2,請求在資源上加S鎖,但該行已經被Query2加上了X鎖;Query2查詢Lock1,請求在資源上加S鎖,但該行已經被Query1加上了X鎖;于是兩個查詢持有資源并互不相讓,構成死鎖。 (4). 解決辦法 a). SQL Server自動選擇一條SQL作死鎖犧牲品:運行完上面的兩個查詢后,我們會發現有一條SQL能正常執行完畢,而另一個SQL則報如下錯誤: 服務器: 消息 1205,級別 13,狀態 50,行 1 事務(進程 ID xx)與另一個進程已被死鎖在 lock 資源上,且該事務已被選作死鎖犧牲品。請重新運行該事務。 這就是上面第四節中介紹的鎖監視器干活了。 b). 按同一順序訪問對象:顛倒任意一條SQL中的update與select語句的順序。例如修改第二條SQL成如下: --Query2 Begin Tran select * from Lock1--在Lock1上申請S鎖 WaitFor Delay '00:01:00'; update Lock2 Set C1=C1+1;--Lock2:RID:X Rollback Tran; 當然這樣修改也是有代價的,這會導致第一條SQL執行完畢之前,第二條SQL一直處于阻塞狀態。單獨執行Query1或Query2需要約1分鐘,但如果開始執行Query1時,馬上同時執行Query2,則Query2需要2分鐘才能執行完;這種按順序請求資源從一定程度上降低了并發性。 c). select語句加With(NoLock)提示:默認情況下select語句會對查詢到的資源加S鎖(共享鎖),S鎖與X鎖(排他鎖)不兼容;但加上With(NoLock)后,select不對查詢到的資源加鎖(或者加Sch-S鎖,Sch-S鎖可以與任何鎖兼容);從而可以是這兩條SQL可以并發地訪問同一資源。當然,此方法適合解決讀與寫并發死鎖的情況,但加With(NoLock)可能會導致臟讀。 select * from Lock2 WITH(NOLock) select * from Lock1 WITH(NOLock) d). 使用較低的隔離級別。SQL Server 2000支持四種事務處理隔離級別(TIL),分別為:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE;SQL Server 2005中增加了SNAPSHOT TIL。默認情況下,SQL Server使用READ COMMITTED TIL,我們可以在上面的兩條SQL前都加上一句SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED,來降低TIL以避免死鎖;事實上,運行在READ UNCOMMITTED TIL的事務,其中的select語句不對結果資源加鎖或加Sch-S鎖,而不會加S鎖;但還有一點需要注意的是:READ UNCOMMITTED TIL允許臟讀,雖然加上了降低TIL的語句后,上面兩條SQL在執行過程中不會報錯,但執行結果是一個返回1,一個返回2,即讀到了臟數據,也許這并不是我們所期望的。 e). 在SQL前加SET LOCK_TIMEOUT timeout_period,當請求鎖超過設定的timeout_period時間后,就會終止當前SQL的執行,犧牲自己,成全別人。 f). 使用基于行版本控制的隔離級別(SQL Server 2005支持):開啟下面的選項后,select不會對請求的資源加S鎖,不加鎖或者加Sch-S鎖,從而將讀與寫操作之間發生的死鎖幾率降至最低;而且不會發生臟讀。 SET ALLOW_SNAPSHOT_ISOLATION ON SET READ_COMMITTED_SNAPSHOT ON g). 使用綁定連接(使用方法見下一個示例。)
5.2 程序死鎖(SQL阻塞) 看一個例子:一個典型的數據庫操作事務死鎖分析,按照我自己的理解,我覺得這應該算是C#程序中出現死鎖,而不是數據庫中的死鎖;下面的代碼模擬了該文中對數據庫的操作過程: //略去的無關的code SqlConnection conn = new SqlConnection(connectionString); conn.Open(); SqlTransaction tran = conn.BeginTransaction(); string sql1 = "update Lock1 SET C1=C1+1"; string sql2 = "select * from Lock1"; executeNonQuery(tran, sql1); //使用事務:事務中Lock了Table executeNonQuery(null, sql2); //新開一個connection來讀取Table
{ SqlCommand cmd = new SqlCommand(sql); if (tran != null) { cmd.Connection = tran.Connection; cmd.Transaction = tran; cmd.executeNonQuery(); } else { using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); cmd.Connection = conn; cmd.executeNonQuery(); } } } 執行到executeNonQuery(null, sql2)時拋出SQL執行超時的異常,下圖從數據庫的角度來看該問題:
代碼從上往下執行,會話1持有了表Lock1的X鎖,且事務沒有結束,回話1就一直持有X鎖不釋放;而會話2執行select操作,請求在表Lock1上加S鎖,但S鎖與X鎖是不兼容的,所以回話2的被阻塞等待,不在等待中,就在等待中獲得資源,就在等待中超時。。。從中我們可以看到,里面并沒有出現死鎖,而只是select操作被阻塞了。也正因為不是數據庫死鎖,所以SQL Server的鎖監視器無法檢測到死鎖。 我們再從C#程序的角度來看該問題:
C#程序持有了表Lock1上的X鎖,同時開了另一個SqlConnection還想在該表上請求一把S鎖,圖中已經構成了環路;太貪心了,結果自己把自己給鎖死了。。。 雖然這不是一個數據庫死鎖,但卻是因為數據庫資源而導致的死鎖,上例中提到的解決死鎖的方法在這里也基本適用,主要是避免讀操作被阻塞,解決方法如下: a). 把select放在update語句前:select不在事務中,且執行完畢會釋放S鎖; tran = connection.BeginTransaction(); string sql1 = "update Lock1 SET C1=C1+1"; executeNonQuery(tran, sql1); //使用事務:事務中Lock了測試表Lock1 string sql2 = @"DECLARE @Token varchar(255); exec sp_getbindtoken @Token out;select @Token;"; string token = executeScalar(tran, sql2).ToString(); string sql3 = "exec sp_bindsession @Token;update Lock1 SET C1=C1+1;exec sp_bindsession null;"; SqlParameter parameter = new SqlParameter("@Token", SqlDbType.VarChar); parameter.Value = token; executeNonQuery(null, sql3, parameter); //新開一個connection來操作測試表Lock1 tran.Commit(); 附:鎖兼容性(from SQL Server 2005 聯機叢書) 鎖兼容性控制多個事務能否同時獲取同一資源上的鎖。如果資源已被另一事務鎖定,則僅當請求鎖的模式與現有鎖的模式相兼容時,才會授予新的鎖請求。如果請求鎖的模式與現有鎖的模式不兼容,則請求新鎖的事務將等待釋放現有鎖或等待鎖超時間隔過期。
該文章在 2023/11/28 10:56:27 編輯過 |
關鍵字查詢
相關文章
正在查詢... |