產生死鎖的四個必要條件:html
(1) 互斥條件:一個資源每次只能被一個進程使用。
(2) 請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。
(3) 不剝奪條件:進程已得到的資源,在末使用完以前,不能強行剝奪。
(4) 循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。sql
二 鎖的分類
鎖的類別有兩種分法:
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)。
session
死鎖原理架構
根據操做系統中的定義:死鎖是指在一組進程中的各個進程均佔有不會釋放的資源,但因互相申請被其餘進程所站用不會釋放的資源而處於的一種永久等待狀態。併發
死鎖的四個必要條件:
互斥條件(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)。一個死鎖示例以下圖所示:性能
說明:T一、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). SELECT加With(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();