併發問題,鎖,怎麼處理死鎖,髒數據處理

  1. 死鎖原理 html

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

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

對應到SQL Server中,當在兩個或多個任務中,若是每一個任務鎖定了其餘任務試圖鎖定的資源,此時會形成這些任務永久阻塞,從而出現死鎖;這些資源多是:單行(RID,堆中的單行)、索引中的鍵(KEY,行鎖)、頁(PAG8KB)、區結構(EXT,連續的8)、堆或B(HOBT) 、表(TAB,包括數據和索引)、文件(File,數據庫文件)、應用程序專用資源(APP)、元數據(METADATA)、分配單元(Allocation_Unit)、整個數據庫(DB)一個死鎖示例以下圖所示: 服務器


    說明:
T1T2表示兩個任務;R1R2表示兩個資源;由資源指向任務的箭頭(R1->T1R2->T2)表示該資源被改任務所持有;由任務指向資源的箭頭(T1->S2T2->S1)表示該任務正在請求對應目標資源;
    其知足上面死鎖的四個必要條件:
(1).互斥:資源S1S2不能被共享,同一時間只能由一個任務使用;
(2).請求與保持條件:T1持有S1的同時,請求S2T2持有S2的同時請求S1
(3).非剝奪條件:T1沒法從T2上剝奪S2T2也沒法從T1上剝奪S1
(4).循環等待條件:上圖中的箭頭構成環路,存在循環等待。 session

 

2. 死鎖排查 併發

(1). 使用SQL Server的系統存儲過程sp_whosp_lock,能夠查看當前數據庫中的鎖狀況;進而根據objectID(@objID)(SQL Server 2005)/ object_name(@objID)(Sql Server 2000)能夠查看哪一個資源被鎖,用dbcc ld(@blk),能夠查看最後一條發生給SQL ServerSql語句; app

複製代碼
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來進行綁定(最後的示例中演示了綁定鏈接) spa

 

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)

執行完WaitforQuery1查詢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中的UpdateSELECT語句的順序。例如修改第二條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一直處於阻塞狀態。單獨執行Query1Query2須要約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 UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLESQL 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持有了表Lock1X鎖,且事務沒有結束,回話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();
複製代碼

 

 

附:鎖兼容性(FROM SQL Server 2005 聯機叢書)

鎖兼容性控制多個事務可否同時獲取同一資源上的鎖。若是資源已被另外一事務鎖定,則僅當請求鎖的模式與現有鎖的模式相兼容時,纔會授予新的鎖請求。若是請求鎖的模式與現有鎖的模式不兼容,則請求新鎖的事務將等待釋放現有鎖或等待鎖超時間隔過時。

相關文章
相關標籤/搜索