ADO.NET事宜拾遗

ADO.NET事务拾遗
代码段1:
using (SqlConnection conn1 = new SqlConnection(connString))
{
  if (conn1.State == ConnectionState.Closed)
      conn1.Open();
   SqlTransaction trans = conn1.BeginTransaction(IsolationLevel.ReadUncommitted);              
   try
   { 
      SqlCommand cmd1 = new SqlCommand();
      cmd1.Connection = conn1;
      cmd1.CommandText = insertString1;//插入语句1
      cmd1.Transaction = trans;
      cmd1.ExecuteNonQuery();
      cmd1.CommandText = insertString2;//插入语句2
      cmd1.Transaction = trans;
      cmd1.ExecuteNonQuery();

      SqlCommand cmd2 = new SqlCommand(selectString, conn1);//查询语句
      SqlDataAdapter sda = new SqlDataAdapter(cmd2);
      DataSet ds =new DataSet();
      sda.Fill(ds);

      trans.Commit();                    
    }
    catch(SqlException ex){
       trans.Rollback();
        Console.WriteLine(ex.Message);
    }
}

说明:insertString1、insertString2和selectString操作的是同一张表
1.运行这段代码,到第21行(sda.Fill(ds))会出现异常:“如果分配给命令的连接位于本地挂起事务中,ExecuteReader 要求命令拥有事务。命令的 Transaction 属性尚未初始化。”
原因:SqlDataAdapter在填充数据时使用SqlDataReader读取数据,事务是基于数据库连接的,cmd1和cmd2使用的是同一连接,执行完cmd1后,事务处于挂起状态,还未提交。为了保持事务的完整性(其实这个地方是原子性的体现),要求新的cmd2必须也使用同一事务。
代码段2:将代码段1稍作修改,使cmd2使用新的连接conn2
using (SqlConnection conn1 = new SqlConnection(connString))
{
  if (conn1.State == ConnectionState.Closed)
      conn1.Open();
   SqlTransaction trans = conn1.BeginTransaction(IsolationLevel.ReadUncommitted);              
   try
   { 
      SqlCommand cmd1 = new SqlCommand();
      cmd1.Connection = conn1;
      cmd1.CommandText = insertString1;//插入语句1
      cmd1.Transaction = trans;
      cmd1.ExecuteNonQuery();
      cmd1.CommandText = insertString2;//插入语句2
      cmd1.Transaction = trans;
      cmd1.ExecuteNonQuery();
      //下面使cmd2使用新的连接conn2
      using (SqlConnection conn2 = new SqlConnection(connString))
      {
        SqlCommand cmd2 = new SqlCommand(selectString, conn2);
        SqlDataAdapter sda = new SqlDataAdapter(cmd2);
        DataSet ds = new DataSet();
        sda.Fill(ds);
      }

      trans.Commit();                    
    }
    catch(SqlException ex){
       trans.Rollback();
        Console.WriteLine(ex.Message);
    }
}

在 sda.Fill(ds)时会抛出异常:“ 超时时间已到。在操作完成之前超时时间已过或服务器未响应”
原因:
在SQL Server 2005中运行下面的脚本,可以获得锁的信息:
select request_session_id,
	   resource_type,
       resource_database_id,
       resource_description ,
	   resource_associated_entity_id,
	   request_mode,
	   request_status       
from sys.dm_tran_locks

会发现有两个会话(SQl Server中的两个进程)发生了冲突,会话1请求了一个排它锁,而会话2一直在等待同一资源的共享锁,因此会话2被阻塞,形成了死锁的情况,过了程序设置的超时时间,SQLServer2005会自动终止操作,在C#中就会捕捉到超时的异常。(虽然会话1中事务的隔离级别已手动设置为未提交读ReadUncommitted,但是回话2中默认隔离级别为已提交读ReadCommited,所以会话2依然会等待会话释放排它锁)

总结:以上例子仅供说明之用,在使用事务时,要根据业务设计好数据库操作顺序,并且恰当的设置隔离级别,避免此种情况的出现。
1 楼 风过有声 2011-04-21  
好 ADO.NET事宜拾遗