使用事务范围实现隐式事务

TransactionScope 类提供了一种将代码块标记为参与事务的简单方法,而无需与事务本身进行交互。 事务范围可以自动选择和管理环境事务。 由于它的易用性和效率,建议在开发事务应用程序时使用该 TransactionScope 类。

此外,还无需将资源显式登记到事务。 任何 System.Transactions 资源管理器(如 SQL Server 2005)都可以检测范围所创建的环境事务是否存在,并自动对其进行登记。

创建事务范围

下面的示例演示了 TransactionScope 类的简单用法。

// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
    string connectString1, string connectString2,
    string commandText1, string commandText2)
{
    // Initialize the return value to zero and create a StringWriter to display results.
    int returnValue = 0;
    System.IO.StringWriter writer = new System.IO.StringWriter();

    try
    {
        // Create the TransactionScope to execute the commands, guaranteeing
        // that both commands can commit or roll back as a single unit of work.
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // Create the SqlCommand object and execute the first command.
                SqlCommand command1 = new SqlCommand(commandText1, connection1);
                returnValue = command1.ExecuteNonQuery();
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue);

                // If you get here, this means that command1 succeeded. By nesting
                // the using block for connection2 inside that of connection1, you
                // conserve server and network resources as connection2 is opened
                // only when there is a chance that the transaction can commit.
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                {
                    // The transaction is escalated to a full distributed
                    // transaction when connection2 is opened.
                    connection2.Open();

                    // Execute the second command in the second database.
                    returnValue = 0;
                    SqlCommand command2 = new SqlCommand(commandText2, connection2);
                    returnValue = command2.ExecuteNonQuery();
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
                }
            }

            // The Complete method commits the transaction. If an exception has been thrown,
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();
        }
    }
    catch (TransactionAbortedException ex)
    {
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
    }

    // Display messages.
    Console.WriteLine(writer.ToString());

    return returnValue;
}
'  This function takes arguments for 2 connection strings and commands to create a transaction
'  involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
'  transaction is rolled back. To test this code, you can connect to two different databases
'  on the same server by altering the connection string, or to another 3rd party RDBMS
'  by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
  ByVal connectString1 As String, ByVal connectString2 As String, _
  ByVal commandText1 As String, ByVal commandText2 As String) As Integer

    ' Initialize the return value to zero and create a StringWriter to display results.
    Dim returnValue As Integer = 0
    Dim writer As System.IO.StringWriter = New System.IO.StringWriter

    Try
        ' Create the TransactionScope to execute the commands, guaranteeing
        '  that both commands can commit or roll back as a single unit of work.
        Using scope As New TransactionScope()
            Using connection1 As New SqlConnection(connectString1)
                ' Opening the connection automatically enlists it in the
                ' TransactionScope as a lightweight transaction.
                connection1.Open()

                ' Create the SqlCommand object and execute the first command.
                Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
                returnValue = command1.ExecuteNonQuery()
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue)

                ' If you get here, this means that command1 succeeded. By nesting
                ' the using block for connection2 inside that of connection1, you
                ' conserve server and network resources as connection2 is opened
                ' only when there is a chance that the transaction can commit.
                Using connection2 As New SqlConnection(connectString2)
                    ' The transaction is escalated to a full distributed
                    ' transaction when connection2 is opened.
                    connection2.Open()

                    ' Execute the second command in the second database.
                    returnValue = 0
                    Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
                    returnValue = command2.ExecuteNonQuery()
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
                End Using
            End Using

            ' The Complete method commits the transaction. If an exception has been thrown,
            ' Complete is called and the transaction is rolled back.
            scope.Complete()
        End Using
    Catch ex As TransactionAbortedException
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message)
    End Try

    ' Display messages.
    Console.WriteLine(writer.ToString())

    Return returnValue
End Function

创建新 TransactionScope 对象后,将启动事务范围。 如代码示例所示,建议使用 using 语句创建范围。 该 using 语句在 C# 和 Visual Basic 中都可用,类似于 try...finally 块,以确保范围得到了适当的处理。

实例化 TransactionScope时,事务管理器确定要参与的事务。 一旦确定,该范围将始终参与该事务。 决策基于两个因素:环境事务是否存在以及构造函数中的参数值 TransactionScopeOption 。 环境事务是代码在其中执行的事务。 可通过调用 Transaction.Current 类的静态 Transaction 属性,获取对环境事务的引用。 有关如何使用此参数的详细信息,请参阅本主题的 TransactionScopeOption 部分的“使用 TransactionScopeOption 管理事务流 ”。

完成事务范围

当应用程序完成要在事务中执行的所有工作时,应仅调用 TransactionScope.Complete 该方法一次,以通知事务管理器提交事务是可以接受的。 将对 Complete 的调用作为 using 块中的最后一个语句,这是很好的做法。

未能调用此方法中止事务,因为事务管理器将此解释为系统失败,或等效于在事务范围内引发的异常。 但是,调用此方法并不保证会提交事务。 这仅仅是向事务管理器告知您的状态的一种方式。 调用 Complete 该方法后,不能再使用 Current 属性访问环境事务,并且尝试这样做将导致引发异常。

TransactionScope如果对象最初创建了事务,则事务管理器提交事务的实际工作发生在块中的using最后一行代码之后。 如果未创建事务,则每当Commit对象的所有者调用CommittableTransaction时,提交就会发生。 此时,事务管理器会调用资源管理器,并根据是否在Complete对象上调用TransactionScope方法来通知他们是提交还是回滚。

using 语句确保在发生异常时仍然调用 Dispose 对象的 TransactionScope 方法。 Dispose 方法标记了事务范围的结束。 调用此方法后发生的异常可能不会影响事务。 此方法还会将环境事务还原到以前的状态。

如果范围创建事务,则会引发 TransactionAbortedException,从而中止事务。 如果事务管理器无法做出提交决定,则会引发 TransactionInDoubtException。 如果已提交事务,则不会引发异常。

回滚事务

如果要回滚事务,则不应在事务范围内调用 Complete 该方法。 例如,可以在该范围中引发异常。 这样,就会回滚该范围所参与的事务。

使用 TransactionScopeOption 管理事务流

可通过调用一个方法来嵌套事务范围,该方法在使用其自己范围的方法中使用 TransactionScope,下面示例中的 RootMethod 方法就是前者这样的方法。

void RootMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        SomeMethod();
        scope.Complete();
    }
}

void SomeMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        scope.Complete();
    }
}

最顶层的事务范围称为根范围。

TransactionScope 类提供多个重载构造函数,这些构造函数接受类型的 TransactionScopeOption枚举,该枚举定义范围的事务行为。

对象 TransactionScope 有三个选项:

  • 联接环境事务,或者在环境事务不存在的情况下创建新的环境事务。

  • 成为新的根范围,也就是说,启动一个新事务并使该事务成为其自己范围中的新环境事务。

  • 根本不参与交易。 因此没有环境事务。

如果用 Required 实例化范围并且存在环境事务,则该范围会联接该事务。 相反,如果不存在环境事务,该范围就会创建新的事务并成为根范围。 这是默认值。 在使用 Required 时,无论范围是根范围还是仅联接环境事务,该范围中的代码都不需要有不同的行为。 这两种情况下的操作应该是相同的。

如果用 RequiresNew 实例化范围,则它始终为根范围。 此操作启动一个新事务,该事务将在作用域内成为新的环境事务。

如果作用域被实例化为 Suppress,那么无论是否存在环境事务,它都不会参与事务。 用此值实例化的范围始终会将 null 作为其环境事务。

下表汇总了上述选项。

TransactionScopeOption 参与环境事务 范围参与
必选 参与新事务(将成为根范围)
需要新 参与新事务(将成为根范围)
抑制 没有交易
必选 是的 参与环境事务
需要新 是的 参与新事务(将成为根范围)
抑制 是的 没有交易

TransactionScope 对象联接现有环境事务时,除非范围中止该事务,否则释放范围对象的操作可能并不会结束事务。 如果环境事务是由根范围创建的,则仅当释放根范围时,才会对事务调用 Commit。 如果事务是手动创建的,则它将在中止或由其创建者提交时结束。

以下示例演示一个对象,该对象创建三个 TransactionScope 嵌套范围对象,每个对象都使用不同的 TransactionScopeOption 值实例化。

using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
    using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
    {
        //...
    }

    using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        //...  
    }
  
    using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
    {
        //...  
    }
}

下面的示例演示一个不包含任何环境事务的代码块,它使用 scope1 创建了一个新范围 (Required)。 该作用域是根范围 scope1 ,因为它会创建新的事务(事务 A),并使事务 A 成为环境事务。 Scope1 然后创建另外三个对象,每个对象具有不同的 TransactionScopeOption 值。 例如,scope2 是使用 Required 创建的,并且由于存在环境事务,它加入了由 scope1 创建的第一个事务。 请注意,scope3 是新事务的根范围,而 scope4 则没有环境事务。

虽然默认值和最常用的值 TransactionScopeOptionRequired,但其他每个值都有其唯一用途。

事务范围内的非事务性代码

Suppress 如果要保留代码块执行的操作,并且在操作失败时不希望中止环境事务,则非常有用。 例如,在要执行日志记录或审核操作时,或者在无论环境事务提交还是中止都要将事件发布给订户时。 此值允许你在事务范围内具有非事务性代码部分,如以下示例所示。

using(TransactionScope scope1 = new TransactionScope())
{
    try
    {
        //Start of non-transactional section
        using(TransactionScope scope2 = new
            TransactionScope(TransactionScopeOption.Suppress))  
        {  
            //Do non-transactional work here  
        }  
        //Restores ambient transaction here
   }
   catch {}  
   //Rest of scope1
}

在嵌套范围中投票

尽管嵌套范围可以联接根范围的环境事务,但在嵌套作用域中调用 Complete 不会影响根范围。 仅当从根范围到最后一个嵌套范围的所有范围都投票决定提交事务时,才会提交该事务。 不在嵌套作用域中调用 Complete 会影响根作用域,因为环境事务将会立即中止。

设置 TransactionScope 超时

TransactionScope 的有些重载构造函数接受 TimeSpan 类型的值,该值用于控制事务的超时。 将超时时间设置为零表示该超时为无限制。 无限长的超时主要对调试有用,调试过程中可能要经由逐句通过代码来隔离业务逻辑中的问题,并且在尝试确定问题期间不希望所调试的事务超时。 在所有其他情况下使用无限长的超时时一定要格外小心,因为它会覆盖防止事务死锁的保护。

通常,在两种情况下,将 TransactionScope 超时设置为默认值以外的值。 第一种是在开发期间,当你想要测试应用程序处理中止事务的方式时。 通过将超时设置为小值(例如 1 毫秒),会导致事务失败,因此可以观察错误处理代码。 将超时值设置为小于默认超时值的第二种情况是:认为在导致死锁的资源争用中涉及到范围时。 在这种情况下,需要尽快地中止事务,而不能等到达到默认超时值。

当范围加入一个环境事务,但指定的超时时间比环境事务的设置时间更短时,会在TransactionScope对象上强制执行新的较短超时,范围必须在指定的嵌套时间内结束,否则事务将被自动中断。 如果嵌套范围的超时值大于环境事务的超时值,则前者无效。

设置 TransactionScope 隔离级别

除超时值之外,TransactionScope 的有些重载构造函数还接受 TransactionOptions 类型的结构,用于指定隔离级别。 默认情况下,事务执行时隔离级别设置为 Serializable. 选择 Serializable 以外的隔离级别通常用于读取密集型系统。 这需要深入了解事务处理理论和事务本身的语义、所涉及的并发问题以及系统一致性的后果。

此外,并非所有资源管理器都支持所有级别的隔离,他们可以选择在比配置的级别更高的级别参与事务。

除了 Serializable,其他每个隔离级别都容易受到访问相同信息的其他事务导致的不一致影响。 不同隔离级别之间的差异在于读取和写入锁的使用方式。 可在事务访问资源管理器中的数据时保持锁定,也可在提交或中止事务之前保持锁定。 前者更适合于吞吐量,后者更适合于一致性。 这两种锁和两种操作(读/写)提供四种基本隔离级别。 有关详细信息,请参阅 IsolationLevel

使用嵌套对象时,必须将所有嵌套 TransactionScope 作用域配置为使用完全相同的隔离级别(如果它们想要联接环境事务)。 如果嵌套 TransactionScope 对象试图加入环境事务,但指定了不同的隔离级别,则会抛出 ArgumentException

与 COM+ 交互

创建新 TransactionScope 实例时,可以使用 EnterpriseServicesInteropOption 其中一个构造函数中的枚举来指定如何与 COM+交互。 有关详细信息,请参阅 与企业服务和 COM+ 事务的互作性

另请参阅