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