死锁总结

来源:互联网 发布:chinaz 源码 编辑:程序博客网 时间:2024/05/29 07:02

1. 原理 

    根据操作系中的定:死是指在一组进程中的各个程均占有不会放的源,但因互相申被其他程所站用不会放的源而于的一永久等待状

    的四个必要条件:
互斥条件(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).
等待条件:上中的箭构成路,存在循等待。

2.

(1). 使用SQL Server的系储过sp_whosp_lock,可以看当前数据中的情况;而根据objectID(@objID)(SQL Server 2005)/ object_name(@objID)(Sql Server 2000)可以看哪个源被,用dbcc ld(@blk),可以看最后一条SQL ServerSql句;
(2). 使用 SQL Server Profiler 分析死: Deadlock graph 事件添加到跟踪。此事件使用死锁涉及到的程和象的 XML 数据填充跟踪中的 TextData 数据列。SQL Server 事件探 可以将 XML 文档提取到死 XML (.xdl) 文件中,以后可在 SQL Server Management Studio 文件。

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  --
住了那个idobjid 

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;

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看那个spidwait,然后用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加上了XQuery2查询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上同时执Query2Query2需要2才能行完;这种源从一定程度上降低了并性。

c). SELECT句加With(NoLock)提示:默情况下SELECT句会对查询到的源加S(共享)SX(排他)不兼容;但加上With(NoLock)后,SELECT对查询到的源加(或者加Sch-SSch-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); //
使用事
:LockTable
ExecuteNonQuery(null, sql2); //
一个connectionTable

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放;而会2select操作,求在表Lock1上加S,但SX是不兼容的,所以回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 丛书)

兼容性控制多个事能否同时获取同一源上的。如果源已被另一事务锁定,则仅的模式与的模式相兼容,才会授予新的锁请求。如果的模式与的模式不兼容,则请求新的事将等待或等待时间期。