leves

使一切更加简单!

导航

.net的事务处理


事务处理

实际上所有用于更新数据源的面向商业的应用程序都需要事务处理支持。通过提供四个基本担保,即众所周知的首字缩写ACID:可分性,一致性,分离性,和耐久性,事务处理将用于确保包含在一个或多个数据源中的系统的完整性。

例如,考虑一个基于Web的零售应用程序,它用于处理购买订单。每个订单需要3个完全不同操作,这些操作涉及到3个数据库更新:

  • 库存水准必须减少所订购的数量。
  • 所购买的量必须记入客户的信用等级。
  • 新订单必须增加到数据库中。

这三个不同的操作作为一个单元并自动执行是至关重要的。三个操作必须全部成功,或都不成功--任何一个操作出现误差都将破坏数据完整性。事务处理提供了这种完整性及其它保证。

要进一步了解事务处理过程的基本原则,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpcontransactionprocessingfundamentals.asp

可以采用很多方法将事务管理合并到数据访问代码中。每种方法适合下面两种基本编程模型之一。

  • 手工事务处理。可以直接在组件代码或存储过程中分别编写利用ADO.NET 或 Transact-SQL事务处理支持特性的代码。
  • 自动化(COM+)事务处理。可以向.NET类中增加声明在运行时指定对象事务处理需要的属性。这种模型使你能方便地配置多个组件以使它们在同一事务处理内运行。

尽管自动化事务处理模型极大地简化了分布式事务处理过程,但两种模型都用于执行本地事务处理(即对单个资源管理器如SQL Server 2000执行的事务处理)或分布式事务处理(即,对位于远程计算机上的多个资源管理执行的事务处理)。

你也许会试图利用自动化(COM+)事务处理来从易于编程的模型中获益。在有多个组件执行数据库更新的系统中,这种优点更明显。然而,在很多情况下,应当避免这种事务处理模型所带来的额外开销和性能损失。

本节将指导你根据特定的应用程序环境选择最合适的模型。

选择事务处理模型

在选择事务处理模型前,首先应当考虑是否真正需要事务处理。事务处理是服务器应用程序使用的最昂贵的资源,在不必要使用的地方,它们降低了扩展性。考虑下面用于管理事务处理使用的准则:

  • 只在需要跨一组操作获取锁并需要加强ACID规则时才执行事务处理。
  • 尽可能短地保持事务处理,以最小化维持数据库锁的时间。
  • 永远不要将客户放到事务处理生命周期的控制之中。
  • 不要为单个SQL语句使用事务处理。SQL Server自动把每个语句作为单个事务处理执行。

自动化事务处理与手工事务处理的对比

尽管编程模型已经对自动化事务处理进行了简化,特别是在多个组件执行数据库更新时,但本地事务处理总是相当快,因为它们不需要与微软DTC交互。即使你对单个本地资源管理器(如SQL Server)使用自动化事务处理,也是这种情况(尽管性能损失减少了),因为手式本地事务处理避免了所有不必要的与DTC的进程间通信。

对于下面的情况,需使用手工事务处理:

  • 对单个数据库执行事务处理。

对于下列情况,则宜使用自动事务处理:

  • 需要将单个事务处理扩展到多个远程数据库时。
  • 需要单个事务处理拥有多个资源管理器(如数据库和Windows 2000消息队列(被称为MSMQ)资源管理器)时。

注意 避免混用事务处理模型。最好只使用其中一个。

在性能足够好的应用程序环境中,(甚至对于单个数据库)选择自动化事务处理以简化编程模型,这种做法是合理的。自动化事务处理使多个组件能很容易地执行现一事务处理中的多个操作。

使用手工事务处理

对于手工事务处理,可以直接在组件代码或存储过程中分别编写使用ADO.NET 或 Transact-SQL事务处理支持特性的代码。多数情况下,应选择在存储过程中控制事务处理,因为这种方法提供了更高的封装性,并且在性能方面,此方法与利用ADO.NET 代码执行事务处理兼容。

利用ADO.NET执行手工事务处理

ADO.NET支持事务处理对象,利用此对象可以开始新事务处理过程,并明确控制事务处理是否执行还是回滚。事务处理对象与单个数据库链接相关,可以通过链接对象的BeginTransaction方法获得。调用此方法并不是暗示,接下来的命令是在事务处理上下文中发出的。必须通过设置命令的Transaction属性,明确地将每个命令与事务处理关联起来。可以将多个命令对象与事务处理对象关联,因此在单个事务处理中就针对单个数据库把多个操作进行分组。

关于使用ADO.NET事务处理代码的示例,见附录中如何编码ADO.NET手工事务处理

更多信息

  • ADO.NET手工事务处理的默认分离级别是读联锁,这意味着在读取数据时,数据库控制共享锁,但在事务处理结束前,数据可以被修改。这种情况潜在地会产生不可重复的读取或虚数据。通过将事务处理对象的IsolationLevel属性设置为IsolationLevel枚举类型所定义的一个枚举值,就可改变分离级别。
  • 必须仔细为事务处理选择合适的分离级别。其折衷是数据一致性与性能的比例。最高的分离等级(被序列化了)提供了绝对的数据一致性,但是以系统整体吞吐量为代价。较低的分离等级会使应用程序更易于扩展,但同时增加了因数据不一致而导致出错的可能性。对多数时间读取数据、极少写入数据的系统来说,较低的分离等级是合适的。
  • 关于选择恰当事务处理级别极有价值的信息,见微软出版社名为Inside SQL Server 2000的书,作者Kalen Delaney。

利用存储过程执行手工事务处理

也可以在存储过程中使用Transact-SQL语句直接控制手工事务处理。例如,可以利用包含了Transact-SQL事务处理语句(如BEGIN TRANSACTION、END TRANSACTION及ROLLBACK TRANSACTION)的存储过程执行事务处理。

更多信息

  • 如果需要,可以在存储过程中使用SET TRANSACTION ISOLATION LEVEL语句控制事务处理的分离等级。读联锁是SQL Server的默认设置。关于SQL Server分离级别的更多信息,见SQL Server在线书目“访问和修改关系数据”一节中的分离级别部分。
  • 关于演示如何利用Transact-SQL事务处理语句执行事务更新的代码示例,见附录中的如何利用Transact-SQL执行事务处理

使用自动化事务

自动化事务简化了编程模型,因为它们不需要明确地开始新事务处理过程,或明确执行或取消事务。然而,自动化事务的最大优点是它们能与DTC结合起来,这就使单个事务可以扩展到多个分布式数据源中。在大型分布式应用程序中,这个优点是很重要的。尽管通过手工对DTC直接编程来控制分布式事务是可能的,但自动化事务处理极大的简化了工作量,并且它是为基于组件的系统而设计的。例如,可以方便地以说明方式配置多个组件以执行包含了单个事务处理的任务。

自动化事务依赖于COM+提供的分布式事务处理支持特性。结果,只有服务组件(即从ServicedComponent类中派生的组件)能够使用自动化事务。

要为自动化事务处理配置类,操作如下:

  • 从位于EnterpriseServices名称空间的ServicedComponent类中派生新类。
  • 通过Transaction属性定义类的事务处理需求。来自TransactionOption的枚举值决定了如何在COM+类中配置类。可与此属性一同设置的其它属性包括事务处理分离等级和超时上限。
  • 为了避免必须明确选出事务处理结果,可以用AutoComplete属性对方法进行注释。如果这些方法释放异常,事务将自动取消。注意,如果需要,仍可以直接挑选事务处理结果。更多详情,见本文稍后确定事务处理结果的节。

更多信息

  • 关于COM+自动化事务的更多信息,可在平台SDK文档中搜索“通过COM+的自动化事务”获取。
  • 关于.NE T事务处理类的示例,见附录中的如何编码.NET事务处理

配置事务处理分离级别

用于COM+1.0版--即运行在Windows 2000中的COM+--的事务处理分离级别被序列化了。这样做提供了最高的分离等级,却是以性能为代价的。系统的整体吞吐量被降低了。因为所涉及到的资源管理器(典型地是数据库)在事务处理期间必须保持读和写锁。在此期间,其它所有事务处理都被阻断了,这种情况将对应用程序的扩展能力产生极大冲击。

随微软Windows .NET发行的COM+ 1.5版允许有COM+目录中按组件配置事务处理分离等级。与事务中根组件相关的设置决定了事务处理的分离等级。另外,同一事务流中的内部子组件拥有的事务处理等级必须不能高于要组件所定义的等级。如果不是这样,当子组件实例化时,将导致错误。

对.NET管理类,Transaction属性支持所有的公有Isolation属性。你可以用此属性陈述式地指定一特殊分离等级,如下面的代码所示:

[Transaction(TransactionOption.Supported, Isolation=TransactionIsolationLevel.ReadCommitted)]
public class Account : ServicedComponent
{
  . . .
}

更多信息

关于配置事务处理分离等级及其它Windows .NET COM+增强特性的更多信息,见MSDN杂志2001年8月期的“Windows XP:利用COM+ 1.5的增强特性使你的组件更强壮”一文。

确定事务处理结果

在单个事务流的所有事务处理组件上下文中,自动化事务处理结果由事务取消标志和一致性标志的状态决定。当事务流中的根组件成为非活动状态(并且控制权返回调用者)时,确定事务处理结果。这种情况在图5中得到了演示,此图显示的是一个典型的银行基金传送事务。


图5 事务流上下文

当根对象(在本例中是对象)变为非活动状态,并且客户的方法调用返回时,确定事务处理结果。在任何上下文中的任何一致性标志被设为假,或如果事务处理取消标志设为真,那么底层的物理DTC事务将被取消。

可以以下面两种方式之一从.NET对象中控制事务处理结果:

  • 可以用AutoComplete属性对方法进行注释,并让.NET自动存放将决定事务处理结果投票。如果方法释放异常,利用此属性,一致性标志自动地被设为假(此值最终使事务取消)。如果方法返回而没有释放异常,那么一致性标志将设为真,此值指出组件乐于执行事务。这并没有得到保证,因为它依赖于同一事务流中其它对象的投票。
  • 可以调用ContextUtil类的静态方法SetComplete或 SetAbort,这些方法分别将一致性标志设为真或假。

严重性大于10的SQL Server错误将导致管理数据供应器释放SqlException类型的异常。如果方法缓存并处理异常,就要确保或者通过手工取消了事务,或者方法被标记了[AutoComplete],以保证异常能传递回调用者



如何编码ADO.NET手工事务

下列代码说明如何利用SQL Server. NET数据供应器提供的事务支持来保护事务的支金转帐操作。该操作在位于同一数据库中的两个帐户之间转移支金。

public void TransferMoney( string toAccount, string fromAccount, decimal amount )
{
  
using ( SqlConnection conn = new SqlConnection(
            
"server=(local);Integrated Security=SSPI;database=SimpleBank" ) )
  
{
    SqlCommand cmdCredit 
= new SqlCommand("Credit", conn );
    cmdCredit.CommandType 
= CommandType.StoredProcedure;
    cmdCredit.Parameters.Add( 
new SqlParameter("@AccountNo", toAccount) );
    cmdCredit.Parameters.Add( 
new SqlParameter("@Amount", amount ));

    SqlCommand cmdDebit 
= new SqlCommand("Debit", conn );
    cmdDebit.CommandType 
= CommandType.StoredProcedure;
    cmdDebit.Parameters.Add( 
new SqlParameter("@AccountNo", fromAccount) );
    cmdDebit.Parameters.Add( 
new SqlParameter("@Amount", amount ));

    conn.Open();
    
// Start a new transaction
    using ( SqlTransaction trans = conn.BeginTransaction() )
    
{
      
// Associate the two command objects with the same transaction
      cmdCredit.Transaction = trans;
      cmdDebit.Transaction 
= trans;
      
try
      
{
        cmdCredit.ExecuteNonQuery();
        cmdDebit.ExecuteNonQuery();
        
// Both commands (credit and debit) were successful
        trans.Commit();
      }

      
catch( Exception ex )
      
{
        
// transaction failed
        trans.Rollback();
        
// log exception details . . .
        throw ex;
      }

    }

  }

}


如何利用Transact-SQL执行事务

下列存储过程说明了如何在Transact-SQL过程内执行事务的支金转移操作。

CREATE PROCEDURE MoneyTransfer
@FromAccount char(20),
@ToAccount char(20),
@Amount money
AS
BEGIN TRANSACTION
-- PERFORM DEBIT OPERATION
UPDATE Accounts
SET Balance = Balance - @Amount
WHERE AccountNumber = @FromAccount
IF @@RowCount = 0
BEGIN
  
RAISERROR('Invalid From Account Number'111)
  
GOTO ABORT
END
DECLARE @Balance money
SELECT @Balance = Balance FROM ACCOUNTS
WHERE AccountNumber = @FromAccount
IF @BALANCE < 0
BEGIN
  
RAISERROR('Insufficient funds'111)
  
GOTO ABORT
END
-- PERFORM CREDIT OPERATION
UPDATE Accounts 
SET Balance = Balance + @Amount 
WHERE AccountNumber = @ToAccount
IF @@RowCount = 0
BEGIN
  
RAISERROR('Invalid To Account Number'111)
  
GOTO ABORT
END
COMMIT TRANSACTION
RETURN 0
ABORT:
  
ROLLBACK TRANSACTION
GO



该存储过程使用BEGIN TRANSACTION, COMMIT TRANSACTION,和ROLLBACK TRANSACTION状态手工控制事务。

如何编码事务性的.NET类

下述例子是三种服务性的NET类,它们配置或用于自动事务。每个类都带有Transaction属性,它的值将决定是否启动新事务流或者对象是否共享即时调用程序的数据流。这些元素一起工作来执行银行支金转移。Transfer类配置有RequiresNew事务属性,而Debit和Credit类配置有Required属性。这样,在运行的时候三个对象共享同一个事务。

using System;
using System.EnterpriseServices;

[Transaction(TransactionOption.RequiresNew)]
public class Transfer : ServicedComponent
{
  [AutoComplete]
  
public void Transfer( string toAccount, 
                        
string fromAccount, decimal amount )
  
{
    
try
    
{
      
// Perform the debit operation
      Debit debit = new Debit();
      debit.DebitAccount( fromAccount, amount );
      
// Perform the credit operation
      Credit credit = new Credit();
      credit.CreditAccount( toAccount, amount );
    }

    
catch( SqlException sqlex )
    
{
      
// Handle and log exception details
      
// Wrap and propagate the exception
      throw new TransferException( "Transfer Failure", sqlex );    
    }

  }

}

[Transaction(TransactionOption.Required)]
public class Credit : ServicedComponent
{
  [AutoComplete]
  
public void CreditAccount( string account, decimal amount )
  
{
    SqlConnection conn 
= new SqlConnection(
            
"Server=(local); Integrated Security=SSPI"; database="SimpleBank");
    SqlCommand cmd 
= new SqlCommand("Credit", conn );
    cmd.CommandType 
= CommandType.StoredProcedure;
    cmd.Parameters.Add( 
new SqlParameter("@AccountNo", account) );
    cmd.Parameters.Add( 
new SqlParameter("@Amount", amount ));
    
try
    
{
      conn.Open();
      cmd.ExecuteNonQuery();
    }

    
catch (SqlException sqlex)
    
{
      
// Log exception details here
      throw// Propagate exception
    }

  }

}

[Transaction(TransactionOption.Required)]
public class Debit : ServicedComponent
{
  
public void DebitAccount( string account, decimal amount )
  
{
    SqlConnection conn 
= new SqlConnection(
            
"Server=(local); Integrated Security=SSPI"; database="SimpleBank");
    SqlCommand cmd 
= new SqlCommand("Debit", conn );
    cmd.CommandType 
= CommandType.StoredProcedure;
    cmd.Parameters.Add( 
new SqlParameter("@AccountNo", account) );
    cmd.Parameters.Add( 
new SqlParameter("@Amount", amount ));
    
try
    
{
      conn.Open();
      cmd.ExecuteNonQuery();
    }

    
catch (SqlException sqlex)
    
{
      
// Log exception details here
      throw// Propagate exception back to caller
    }

  }

}

posted on 2005-08-26 13:51  leves  阅读(5509)  评论(2编辑  收藏  举报