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

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 语句创建范围。 C# 和 Visual Basic 中均提供 using 语句,它的工作方式与 try...finally 块类似,可确保正确释放范围。

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

完成事务范围

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

若未能调用此方法,应用程序将中止事务,这是因为事务管理器会将此问题解释为系统故障,或者解释为相当于在事务范围内引发了异常。 但是,调用此方法并不保证会提交事务。 它只是一种将状态通知给事务管理器的方式。 调用 Complete 方法之后,就不能再通过 Current 属性访问环境事务,尝试这样做将会导致引发异常。

如果 TransactionScope 对象最初创建了事务,事务管理器会在 using 块的最后一行代码后执行实际的事务提交工作。 如果该对象未创建事务,则每当 Commit 对象的所有者调用 CommittableTransaction 时都会执行提交。 届时,事务管理器会调用资源管理器,并根据是否对 TransactionScope 对象调用了 Complete 方法来通知资源管理器进行提交或回滚。

using 语句可确保调用 TransactionScope 对象的 Dispose 方法,即使发生异常时也是如此。 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 参与环境事务 范围参与
必选 参与新事务(将成为根范围)
Requires New 参与新事务(将成为根范围)
取消 不参与任何事务
必需 参与环境事务
Requires New 参与新事务(将成为根范围)
取消 不参与任何事务

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 则没有环境事务。

虽然 TransactionScopeOption 的默认值和最常用的值是 Required,但其他各值都有其独有的用途。

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

如果要保留代码部分执行的操作,并且不希望在操作失败时中止环境事务,则 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+ 事务的互操作性

请参阅