再探 System.Transactions

     在 Microsoft .NET Framework 中,System.Transactions 命名空间使得事务的处理比采用以往任何一种技术都要简单。此前,我曾经撰写过一个数据点专栏,介绍了 System.Transactions 在 Microsoft® .NET Framework 2.0 Beta 1 以及 SQL Server™ 2005 下的工作方式。当然,在产品的发布过程中,既增加了一些功能,也去掉了一些功能;有些 TransactionScopeOptions 已经发生了变化。
从那以后,读者们提出了很多有关 System.Transactions 的问题,这也促使我下定决心再探其究竟。下面,我们就来看看它现在的工作方式,我会告诉您如何使用这个命名空间、它在什么情况下有效以及在什么情况下不能发挥作用。通过以下的内容,您将了解到如何充分利用 .NET 架构去更有效地使用命名空间。此外,我还将使用一些事务来演示最佳的实践操作。本文用到的所有示例均可从 MSDN®杂志网站下载。

一语道破天机
    我们先来看看如何将两条数据库命令转换为一个事务,具体方法就是构建一个封装器把这两条命令封装起来。具体操作非常简单。只要引用 System.Transactions.dll,然后把您需要的事务性代码封装在一个 using 语句内,这个 using 语句会创建一个 TransactionScope,最后,在事务结束时调用 Complete 方法。
 
     图 1 所显示的就是一个正在创建的事务,这个事务自身还封装了多个数据库查询。只要任意一个 SqlCommand 对象引发异常,程序流控制就会跳出 TransactionScope 的 using 语句块,随后,TransactionScope 将自行释放并回滚该事务。由于这段代码使用了 using 语句,所以 SqlConnection 对象和 TransactionScope 对象都将被自动释放。由此可见,只需添加很少的几行代码,您就可以构建出一个事务模型,这个模型可以对异常进行处理,执行结束后会自行清理,此外,它还可以对命令的提交或回滚进行管理。
 
Figure 1 A Simple Transaction
代码
// Create the TransactionScope
using (TransactionScope ts = new TransactionScope())
{
    
using (SqlConnection cn2005 = new SqlConnection(someSql2005))
    {
        SqlCommand cmd 
= new SqlCommand(sqlUpdate, cn2005);
        cn2005.Open();
        cmd.ExecuteNonQuery();
    }

    
using (SqlConnection cn2005 = new SqlConnection(anotherSql2005))
    {
        SqlCommand cmd 
= new SqlCommand(sqlDelete, cn2005);
        cn2005.Open();
        cmd.ExecuteNonQuery();
    }

    
// Tell the transaction scope that the transaction is in
    
// a consistent state and can be committed
    ts.Complete();

    
// When the end of the scope is reached, the transaction is
    
// completed, committed, and disposed.
}

 

  
     您在图 1 中所看到的示例还有很多可以灵活设置的部分。TransactionScope 包含了所有的资源管理器连接,这些连接会自动参与到事务中。这样一来,您就可以为 TransactionScope 设置不同的选项,在图 1 的示例中,我们使用的是默认设置。
 
当前事务与参与方式
     TransactionScope 类是 System.Transactions 的核心所在。这个类经过实例化,就会创建出一个当前事务(也称为氛围事务 (ambient transaction)),任何资源管理器都可以参与这个事务。举例来说,假设我们已经创建了 TransactionScope 的一个实例,并打开了与某个资源管理器的连接,这个资源管理器的默认设置是自动参与事务,因此,这个连接也将加入到事务范围内。
您可以在代码的任何位置上随时检查是否存在事务范围,具体方法就是查看 System.Transactions.Transaction.Current 属性。如果这个属性为“null”,说明不存在当前事务。资源管理器在打开它与其资源的连接时,它会检查是否存在事务。如果这个资源管理器已被设置为自动参与当前事务,那么它将加入到这个事务中。SQL Server 连接字符串的属性之一就是 auto-enlist。默认情况下,auto-enlist 设置为 true,因此,它会加入到任何活动事务中。您也可以通过给连接字符串显式添加一个“auto-enlist=false”来改变默认设置,如下所示:
 
Server=(local)\SQL2005;Database=Northwind;
Integrated Security
=SSPI;auto-enlist=false

     这就是 System.Transactions 的神奇之处。我并没有改变图 1 中的任何 ADO.NET 代码,但它却依然能够充分利用 TransactionScope。我所做的只是创建了一个 TransactionScope 对象,以及一个在连接打开后参与到活动事务中的 SqlConnection 对象。
 
事务的设置
      若要更改 TransactionScope 类的默认设置,您可以创建一个 TransactionOptions 对象,然后通过它在 TransactionScope 对象上设置隔离级别和事务的超时时间。TransactionOptions 类有一个 IsolationLevel 属性,通过这个属性可以更改隔离级别,例如从默认的可序列化 (Serializable) 改为 ReadCommitted,甚至可以改为 SQL Server 2005 引入的新的快照 (Snapshot) 级别。(请记住,隔离级别仅仅是一个建议。大多数数据库引擎会试着使用建议的隔离级别,但也可能选择其他级别。)此外,TransactionOptions 类还有一个 TimeOut 属性,这个属性可以用来更改超时时间(默认设置为 1 分钟)。
 

System.Transactions TransactionOptions 类有一个 TimeOut 属性,这个属性可以用来更改超时时间(默认设置为 1 分钟)。可以通过配置文件修改TimeOut的值:

<configuration>
<system.transactions>
<defaultSettings timeout="00:05:00" />
</system.transactions>
</configuration>

 
图 1 示例中使用了默认的 TransactionScope 对象及其默认构造函数。也就是说,它的隔离级别设置为可序列化 (Serializable),事务的超时时间为 1 分钟,而且 TransactionScopeOptions 的设置为 Required。除此以外,还有 7 种用于 TransactionScope 的重载构造函数,您可以使用这些构造函数来更改这些设置。我在图 2 中列出了 TransactionScopeOptions 枚举器的各项设置。这些枚举器使您能够控制嵌套事务彼此之间的响应方式。图 3 中的代码实际上就是更改这些设置。
 
Figure 3 Changing Transactional Settings
代码
// Create the TransactionOptions object
TransactionOptions tOpt = new TransactionOptions();

// Set the Isolation Level
tOpt.IsolationLevel = IsolationLevel.ReadCommitted;

// Set the timeout to be 2 minutes
// Uses the (hours, minutes, seconds) constructor
// Default is 60 seconds
tOpt.Timeout = new TimeSpan(020);

string cnString = ConfigurationManager.ConnectionStrings[
    
"sql2005DBServer"].ConnectionString);

// Create the TransactionScope with the RequiresNew transaction 
// setting and the TransactionOptions object I just created
using (TransactionScope ts =
    
new TransactionScope(TransactionScopeOption.RequiresNew, tOpt))
{
    
using (SqlConnection cn2005 = new SqlConnection(cnString)
    {
        SqlCommand cmd 
= new SqlCommand(updateSql1, cn2005);
        cn2005.Open();
        cmd.ExecuteNonQuery();
    }

    ts.Complete();
}

 

Figure 2 TransactionScopeOptions Enumerators

TransactionScopeOptions 描述
Required 如果已经存在一个事务,那么这个事务范围将加入已有的事务。否则,它将创建自己的事务。
RequiresNew 这个事务范围将创建自己的事务。
Suppress 如果处于当前活动事务范围内,那么这个事务范围既不会加入氛围事务 (ambient transaction),也不会创建自己的事务。当部分代码需要留在事务外部时,可以使用该选项。

 

释放
用好 System.Transactions 的关键就在于了解事务如何结束以及何时结束。如果一个 TransactionScope 对象没有被正确释放,那么这个事务将保持打开状态,直到这个对象被垃圾收集器所收集,或者已超过超时时间为止。对打开的事务置之不理是有一定危险性的,其中之一就是处于活动状态的事务会锁定资源管理器的资源。下面这段代码或许能帮助您更好的理解这个问题:

 

代码
TransactionScope ts = new TransactionScope();
SqlConnection cn2005 
= new SqlConnection(cnString);
SqlCommand cmd 
= new SqlCommand(updateSql1, cn2005);
cn2005.Open();
cmd.ExecuteNonQuery();
cn2005.Close();
ts.Complete();

 

 

     这段代码会创建 TransactionScope 对象的一个实例,当 SqlConnection 打开后,它将加入到该事务中。如果一切顺利,该命令将得到执行,连接将会关闭,事务将会完成,而且它也会被释放掉。但是,如果运行过程引发异常,那么程序流控制就会跳过关闭 SqlConnection 和释放 TransactionScope 的操作,导致该事务在比预期更长的时间内保持打开状态。因此,重中之重就是要确保正确释放 TransactionScope,使事务要么快速提交,要么快速回滚。您可以通过两种简单的方法来处理这个问题:使用 try/catch/finally 代码块,或者使用 using 语句。您可以在 try/catch/finally 代码块之外声明这些对象,在 try 代码块中添加代码来创建对象并执行命令,并将对 TransactionScope 和 SqlConnection 的释放放到 finally 代码块中。这种方法可以确保事务及时关闭。

 

     我本人更喜欢使用 using 语句,因为它能够隐性地为你创建一个 try/catch 代码块。使用 using 语句时,即便代码块中途引发异常,using 语句也能够保证 TransactionScope 将会被释放。无论何时退出代码块,using 语句都会确保已调用了 TransactionScope 的 Dispose 方法。这一点非常重要,因为就在释放 TransactionScope 之前,该事务已经完成了。事务完成时,TransactionScope 就会判断是否已经调用了 Complete 方法。如果已经调用,那么该事务就会被提交;否则,该事务就会回滚。前面部分的代码可以这样来写:
 
代码
using (TransactionScope ts = new TransactionScope())
{
    
using (SqlConnection cn2005 = new SqlConnection(cnString)
    {
        SqlCommand cmd 
= new SqlCommand(updateSql1, cn2005);
        cn2005.Open();
        cmd.ExecuteNonQuery();
    }
    ts.Complete();
}

 

请注意,对 TransactionScope 对象和 SqlConnection 对象,我都使用了 using 语句。这样做是为了确保一旦引发异常,这两个对象都可以得到快速、正确的释放。如果代码块没有引发异常,那么这两个对象将在 using 语句代码块结束时(最后一个大括号的位置)被释放。

 

 

轻型事务
    System.Transactions 的强大功能之一就是它对轻型事务的支持。除非情况需要,否则轻型事务是不会用到 Microsoft 分布式事务处理协调器 (DTC) 的。如果事务是本地的,那么它将是一个轻型事务。如果事务变成了分布式的,而且涉及到了第二个资源管理器,那么这个轻型事务就会提升为一个完全分布式的事务,而且一定要用到 DTC。
 
    在分布式的应用场景中必须使用 DTC,这无疑会大大增加成本。因此,除非万不得已,否则最好避免这种情况。幸运的是,SQL Server 2005 提供了对轻型事务的支持;而 SQL Server 以往的版本都没有这种功能(也就是说,在 SQL Server 2000 下,所有的事务都要升级为分布式事务)。我们可以通过几个例子来体验一下轻型事务带来的益处。
 
    在图 4 的示例中,我所创建的 TransactionScope 会针对一个 SQL Server 2000 数据库执行两条命令。与 SQL Server 2000 的连接打开后,它会加入到这个 TransactionScope;因为 SQL Server 2000 不支持轻型事务,因此必须使用 DTC,尽管这显然不是一个分布式应用场景(因为我只针对一个数据库,通过同一个连接来执行操作)。
 
Figure 4 No Lightweight Support
代码
using (TransactionScope ts = new TransactionScope())
{
    
using (SqlConnection cn2000 = new SqlConnection(cnString2000))
    {
        cn2000.Open();

        SqlCommand cmd1 
= new SqlCommand(updateSql1, cn2000);
        cmd1.ExecuteNonQuery();

        SqlCommand cmd2 
= new SqlCommand(updateSql2, cn2000);
        cmd2.ExecuteNonQuery();
    }
    ts.Complete();
}

 

在下一个例子中(如图 5 所示),我所创建的 TransactionScope 会针对一个 SQL Server 2005 数据库执行两条命令。由于 SQL Server 2005 支持轻型事务,因此,只有当涉及到第二个资源管理器时,它才会变成一个分布式事务。

 

Figure 5 Lightweight Support

 

代码
using (TransactionScope ts = new TransactionScope())
{
    
using (SqlConnection cn2005 = new SqlConnection(cnString2005))
    {
        cn2005.Open();

        SqlCommand cmd1 
= new SqlCommand(updateSql1, cn2005);
        cmd1.ExecuteNonQuery();

        SqlCommand cmd2 
= new SqlCommand(updateSql2, cn2005);
        cmd2.ExecuteNonQuery();
    }
    ts.Complete();
}

 

 

    那么,我们应该在哪些情况下使用 System.Transactions 呢?如果你打算使用分布式事务,那么 System.Transactions 将大有裨益。同样,如果你的资源管理器支持轻型事务,那么 System.Transactions 也将是不二的选择。但是,用 System.Transactions 去包含所有的数据库命令却不一定是最佳的方法。
举例来说,我们假设你的应用程序要针对一个不支持轻型事务的数据库执行多条命令。业务规则规定这些操作需要包含在一个事务中,以保持其原子性。如果该事务中的命令只针对单个数据库,那么 ADO.NET 事务将比 System.Transactions 效率更高,因为在这种情况下,ADO.NET 事务不会调用 DTC。如果你的应用程序中确实需要一些分布式的环节,那么 System.Transactions 就会是一种不错的选择。
那么,System.Transactions 能够支持哪些资源管理器呢?实际上,System.Transactions 可以支持所有的资源管理器,只不过,支持轻型事务的资源管理器能够充分利用可自动提升的事务。
 
嵌套
    在前文中,我已经提到了 TransactionScopeOptions 枚举器以及如何将其设置为 Required(默认值)、RequiresNew 或 Suppress。当你遇到嵌套方法和事务时,这个枚举器就能起到作用了。举例来说,假设 Method1 创建一个 TransactionScope,针对一个数据库执行一条命令,然后调用 Method2。Method2 创建一个自身的 TransactionScope,并针对一个数据库执行另一条命令。您可以通过多种方法来处理这个问题。您可能希望 Method2 的事务加入到 Method1 的事务中,也可能想让 Method2 创建一个属于自己的单独的事务。在这种情况下,TransactionScopeOptions 枚举器的价值就得到了充分体现。图 6 显示的是嵌套事务。
 
Figure 6 Nesting Transactions
代码
private void Method1()
{
    
using (TransactionScope ts = 
        
new TransactionScope(TransactionScopeOption.Required))
    {
        
using (SqlConnection cn2005 = new SqlConnection())
        {
            SqlCommand cmd 
= new SqlCommand(updateSql1, cn2005);
            cn2005.Open();
            cmd.ExecuteNonQuery();
        }
        Method2();
        ts.Complete();
    }
}

private void Method2()
{
    
using (TransactionScope ts = 
        
new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        
using (SqlConnection cn2005 = new SqlConnection())
        {
            SqlCommand cmd 
= new SqlCommand(updateSql2, cn2005);
            cn2005.Open();
            cmd.ExecuteNonQuery();
        }
        ts.Complete();
    }
}

 

 

   在这里,内层事务 (Method2) 将创建出第二个 TransactionScope,而不是加入外层事务(来自 Method1)。Method2 的 TransactionScope 是使用 RequiresNew 设置创建的,也就是告诉这个事务要创建自己的范围,而不是加入一个已有的范围。如果您希望这个事务加入到已有事务中,您可以保留默认设置不变,或者将该选项设置为 Required。
事务加入到一个 TransactionScope(因为它们使用了 Required 设置)中后,只有它们全部投票,事务才能成功完成 (Complete),也才能提交事务。在同一个 TransactionScope 中,如果任何一个事务没有调用 ts.Complete,也就是说没有投票完成 (Complete),那么当外层的 TransactionScope 被释放后,它将会回滚。

 

总结
    进入和退出事务都要快,这一点非常重要,因为事务会锁定宝贵的资源。最佳实践要求我们在需要使用事务之前再去创建它,在需要对其执行命令前迅速打开连接,执行动作查询 (Action Query),并尽可能快地完成和释放事务。在事务执行期间,您还应该避免执行任何不必要的、与数据库无关的代码,这能够防止资源被毫无疑义地锁定过长的时间。
轻型事务的强大功能之一就在于它能够判断出自己是否需要提升为分布式事务。正如我在前面所演示的,正确使用 System.Transactions 能够给您带来很多益处。关键就是要了解如何使用,以及在什么情况下使用。

将您想向 John 询问的问题和提出的意见发送至 mmdata@microsoft.com.

 

 

posted @ 2010-02-08 22:21  唔愛吃蘋果  阅读(623)  评论(0编辑  收藏  举报