1. 程式人生 > >死鎖產生的原因和解鎖的方法

死鎖產生的原因和解鎖的方法

.cn int table using 生死 ima 測試表 varchar command

產生死鎖的四個必要條件:

(1) 互斥條件:一個資源每次只能被一個進程使用。
(2) 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 循環等待條件:若幹進程之間形成一種頭尾相接的循環等待資源關系。

二 鎖的分類

鎖的類別有兩種分法:

1. 從數據庫系統的角度來看:分為獨占鎖(即排它鎖),共享鎖和更新鎖

MS-SQL Server 使用以下資源鎖模式。

鎖模式 描述
共享 (S) :讀鎖,用於不更改或不更新數據的操作(只讀操作),如 SELECT 語句。
更新 (U) :(介於共享和排它鎖之間

),可以讓其他程序在不加鎖的條件下讀,但本程序可以隨時更改。

讀取表時使用更新鎖,而不使用共享鎖,並將鎖一直保留到語句或事務的結束。UPDLOCK 的優點是允許您讀取數據(不阻塞其它事務)並在以後更新數據,同時確保自從上次讀取數據後數據沒有被更改。當我們用UPDLOCK來讀取記錄時可以對取到的記錄加上更新鎖,從而加上鎖的記錄在其它的線程中是不能更改的只能等本線程的事務結束後才能更改,我如下示例:

BEGIN TRANSACTION --開始一個事務
SELECT Qty
 FROM myTable WITH (UPDLOCK)
 WHERE Id in (1,2,3)

 UPDATE myTable SET Qty 
= Qty - A.Qty FROM myTable AS A INNER JOIN @_Table AS B ON A.ID = B.ID COMMIT TRANSACTION --提交事務

這樣在更新時其它的線程或事務在這些語句執行完成前是不能更改ID是1,2,3的記錄的.其它的都可以修改和讀,1,2,3的只能讀,要是修改的話只能等這些語句完成後才能操作.從而保證的數據的修改正確.


排它 (X):寫鎖。 用於數據修改操作,例如 INSERT、UPDATE 或 DELETE。確保不會同時同一資源進行多重更新。
意向鎖 用於建立鎖的層次結構。意向鎖的類型為:意向共享 (IS)、意向排它 (IX) 以及與意向排它共享 (SIX)。


架構鎖 在執行依賴於表架構的操作時使用。架構鎖的類型為:架構修改 (Sch-M) 和架構穩定性 (Sch-S)。
大容量更新 (BU) 向表中大容量復制數據並指定了 TABLOCK 提示時使用。

共享鎖
共享 (S) 鎖允許並發事務讀取 (SELECT) 一個資源。資源上存在共享 (S) 鎖時,任何其它事務都不能修改數據。一旦已經讀取數據,便立即釋放資源上的共享 (S) 鎖,除非將事務隔離級別設置為可重復讀或更高級別,或者在事務生存周期內用鎖定提示保留共享 (S) 鎖。

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

若要避免這種潛在的死鎖問題,請使用更新 (U) 鎖。一次只有一個事務可以獲得資源的更新 (U) 鎖。如果事務修改資源,則更新 (U) 鎖轉換為排它 (X) 鎖。否則,鎖轉換為共享鎖。

排它鎖
排它 (X) 鎖可以防止並發事務對資源進行訪問。其它事務不能讀取或修改排它 (X) 鎖鎖定的數據。

意向鎖
意向鎖表示 SQL Server 需要在層次結構中的某些底層資源上獲取共享 (S) 鎖或排它 (X) 鎖。例如,放置在表級的共享意向鎖表示事務打算在表中的頁或行上放置共享 (S) 鎖。在表級設置意向鎖可防止另一個事務隨後在包含那一頁的表上獲取排它 (X) 鎖。意向鎖可以提高性能,因為 SQL Server 僅在表級檢查意向鎖來確定事務是否可以安全地獲取該表上的鎖。而無須檢查表中的每行或每頁上的鎖以確定事務是否可以鎖定整個表。

意向鎖包括意向共享 (IS)、意向排它 (IX) 以及與意向排它共享 (SIX)。


死鎖原理

根據操作系統中的定義:死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處於的一種永久等待狀態。

死鎖的四個必要條件:
互斥條件(Mutual exclusion):資源不能被共享,只能由一個進程使用。
請求與保持條件(Hold and wait):已經得到資源的進程可以再次申請新的資源。
非剝奪條件(No pre-emption):已經分配的資源不能從相應的進程中被強制地剝奪。
循環等待條件(Circular wait):系統中若幹進程組成環路,該環路中每個進程都在等待相鄰進程正占用的資源。

對應到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)表示該任務正在請求對應目標資源;
其滿足上面死鎖的四個必要條件:
(1).互斥:資源S1和S2不能被共享,同一時間只能由一個任務使用;
(2).請求與保持條件:T1持有S1的同時,請求S2;T2持有S2的同時請求S1;
(3).非剝奪條件:T1無法從T2上剝奪S2,T2也無法從T1上剝奪S1;
(4).循環等待條件:上圖中的箭頭構成環路,存在循環等待。

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);

CREATE Table #Lock(spid int,
dpid int,
objid int,
indld int,
[Type] nvarchar(20),
Resource nvarchar(50),
Mode nvarchar(10),
Status nvarchar(10)
);

INSERT INTO #Who
EXEC sp_who active --看哪個引起的阻塞,blk
INSERT INTO #Lock
EXEC sp_lock --看鎖住了那個資源id,objid

DECLARE @DBName nvarchar(20);
SET @DBName=‘NameOfDataBase‘

SELECT #Who.* FROM #Who WHERE dbname=@DBName
SELECT #Lock.* FROM #Lock
JOIN #Who
ON #Who.spid=#Lock.spid
AND dbname=@DBName;

--最後發送到SQL Server的語句
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 #Who;
DROP Table #Lock;


(2). 使用 SQL Server Profiler 分析死鎖: 將 Deadlock graph 事件類添加到跟蹤。此事件類使用死鎖涉及到的進程和對象的 XML 數據填充跟蹤中的 TextData 數據列。SQL Server 事件探查器 可以將 XML 文檔提取到死鎖 XML (.xdl) 文件中,以後可在 SQL Server Management Studio 中查看該文件。

3. 避免死鎖

上面1中列出了死鎖的四個必要條件,我們只要想辦法破其中的任意一個或多個條件,就可以避免死鎖發生,一般有以下幾種方法(FROM Sql Server 2005聯機叢書):
(1).按同一順序訪問對象。(註:避免出現循環)
(2).避免事務中的用戶交互。(註:減少持有資源的時間,較少鎖競爭)
(3).保持事務簡短並處於一個批處理中。(註:同(2),減少持有資源的時間)
(4).使用較低的隔離級別。(註:使用較低的隔離級別(例如已提交讀)比使用較高的隔離級別(例如可序列化)持有共享鎖的時間更短,減少鎖競爭)
(5).使用基於行版本控制的隔離級別:2005中支持快照事務隔離和指定READ_COMMITTED隔離級別的事務使用行版本控制,可以將讀與寫操作之間發生的死鎖幾率降至最低:
SET ALLOW_SNAPSHOT_ISOLATION ON --事務可以指定 SNAPSHOT 事務隔離級別;
SET READ_COMMITTED_SNAPSHOT ON --指定 READ_COMMITTED 隔離級別的事務將使用行版本控制而不是鎖定。默認情況下(沒有開啟此選項,沒有加with nolock提示),SELECT語句會對請求的資源加S鎖(共享鎖);而開啟了此選項後,SELECT不會對請求的資源加S鎖。
註意:設置 READ_COMMITTED_SNAPSHOT 選項時,數據庫中只允許存在執行 ALTER DATABASE 命令的連接。在 ALTER DATABASE 完成之前,數據庫中決不能有其他打開的連接。數據庫不必一定要處於單用戶模式中。
(6).使用綁定連接。(註:綁定會話有利於在同一臺服務器上的多個會話之間協調操作。綁定會話允許一個或多個會話共享相同的事務和鎖(但每個回話保留其自己的事務隔離級別),並可以使用同一數據,而不會有鎖沖突。可以從同一個應用程序內的多個會話中創建綁定會話,也可以從包含不同會話的多個應用程序中創建綁定會話。在一個會話中開啟事務(begin tran)後,調用exec sp_getbindtoken @Token out;來取得Token,然後傳入另一個會話並執行EXEC sp_bindsession @Token來進行綁定(最後的示例中演示了綁定連接)。

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

public static void ExecuteNonQuery(SqlTransaction tran, string sql)
{
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鎖;
b). SELECT也放加入到事務中:ExecuteNonQuery(tran, sql2);
c). SELECTWith(NOLock)提示:可能產生臟讀;
d). 降低事務隔離級別:SELECT語句前加SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;同上,可能產生臟讀;
e). 使用基於行版本控制的隔離級別(同上例)。
g). 使用綁定連接:取得事務所在會話的token,然後傳入新開的connection中;執行EXEC sp_bindsession @Token後綁定了連接,最後執行exec sp_bindsession null;來取消綁定;最後需要註意的四點是:
(1). 使用了綁定連接的多個connection共享同一個事務和相同的鎖,但各自保留自己的事務隔離級別;
(2). 如果在sql3字符串的“exec sp_bindsession null”換成“commit tran”或者“rollback tran”,則會提交整個事務,最後一行C#代碼tran.Commit()就可以不用執行了(執行會報錯,因為事務已經結束了-,-)。
(3). 開啟事務(begin tran)後,才可以調用exec sp_getbindtoken @Token out來取得Token;如果不想再新開的connection中結束掉原有的事務,則在這個connection close之前,必須執行“exec sp_bindsession null”來取消綁定連接,或者在新開的connectoin close之前先結束掉事務(commit/tran)。
(4). (Sql server 2005 聯機叢書)後續版本的 Microsoft SQL Server 將刪除該功能。請避免在新的開發工作中使用該功能,並著手修改當前還在使用該功能的應用程序。 請改用多個活動結果集 (MARS) 或分布式事務。

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();

死鎖產生的原因和解鎖的方法