Partilhar via


Implementando uma transação implícita usando o escopo da transação

A TransactionScope classe fornece uma maneira simples de marcar um bloco de código como participante de uma transação, sem exigir que você interaja com a transação em si. Um escopo de transação pode selecionar e gerenciar a transação ambiente automaticamente. Devido à sua facilidade de uso e eficiência, é recomendável que você use a TransactionScope classe ao desenvolver um aplicativo de transação.

Além disso, você não precisa recrutar recursos explicitamente com a transação. Qualquer System.Transactions gestor de recursos (como o SQL Server 2005) pode detetar a existência de uma transação ambiente criada pelo contexto e inscrever-se automaticamente.

Criar um escopo de transação

O exemplo a seguir mostra um uso simples da TransactionScope classe.

// 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

O escopo da transação é iniciado quando você cria um novo TransactionScope objeto. Conforme ilustrado no exemplo de código, é recomendável criar escopos com uma using instrução. A instrução using está disponível tanto em C# como em Visual Basic e funciona como um bloco try...finally para garantir que o escopo seja liberado corretamente.

Quando o programador instancia TransactionScope, o gestor de transações determina em que transação participar. Uma vez determinado, o escopo sempre participa dessa transação. A decisão é baseada em dois fatores: se uma transação ambiental está presente e o valor do TransactionScopeOption parâmetro no construtor. A transação ambiente é a transação dentro da qual seu código é executado. Você pode obter uma referência à transação ambiente chamando a propriedade estática Transaction.Current da classe Transaction. Para obter mais informações sobre como esse parâmetro é usado, consulte a seção Gerenciando o fluxo de transações usando TransactionScopeOption deste tópico.

Concluindo um escopo de transação

Quando seu aplicativo concluir todo o trabalho que deseja executar em uma transação, você deve chamar o TransactionScope.Complete método apenas uma vez para informar ao gerente de transações que é aceitável confirmar a transação. É uma excelente prática colocar a chamada para Complete como a última declaração no bloco using.

A falha ao chamar esse método anula a transação, porque o gerenciador de transações interpreta isso como uma falha do sistema, ou equivalente a uma exceção lançada dentro do escopo da transação. No entanto, chamar esse método não garante que a transação será confirmada. É apenas uma forma de informar o gestor de transações do seu estado. Depois de chamar o método Complete, não é mais possível aceder à transação ambiental usando a propriedade Current, e tentar fazê-lo resultará numa exceção.

Se o TransactionScope objeto criou a transação inicialmente, o trabalho real de confirmar a transação pelo gerenciador de transações ocorre após a última linha de código no using bloco . Se não criou a transação, a confirmação ocorre sempre que Commit é chamada pelo proprietário do CommittableTransaction objeto. Nesse ponto, o gestor de transações chama os gestores de recursos e informa-os para confirmarem ou reverterem, com base se o método Complete foi chamado no objeto TransactionScope.

A using instrução garante que o Dispose método do TransactionScope objeto seja chamado mesmo se ocorrer uma exceção. O Dispose método marca o fim do escopo da transação. As exceções que ocorrem após a chamada desse método podem não afetar a transação. Esse método também restaura a transação ambiental para o estado anterior.

A TransactionAbortedException é lançado se o escopo cria a transação e a transação é anulada. A TransactionInDoubtException é lançado se o gerenciador de transações não conseguir chegar a uma decisão de confirmação. Nenhuma exceção será lançada se a transação for confirmada.

Reverter uma transação

Se você quiser reverter uma transação, não deve chamar o Complete método dentro do escopo da transação. Por exemplo, você pode lançar uma exceção dentro do escopo. A transação em que participa será revertida.

Gerenciando o fluxo de transações usando TransactionScopeOption

O escopo da transação pode ser aninhado ao chamar um método que usa um TransactionScope dentro de outro método com seu próprio escopo, como no caso do método RootMethod no exemplo a seguir,

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();
    }
}

O escopo de transação mais alto é conhecido como o escopo raiz.

A TransactionScope classe fornece vários construtores sobrecarregados que aceitam uma enumeração do tipo TransactionScopeOption, que define o comportamento transacional do escopo.

Um TransactionScope objeto tem três opções:

  • Participe na transação ambiental ou crie uma nova, se não existir.

  • Seja um novo escopo raiz, ou seja, inicie uma nova transação e faça com que essa transação seja a nova transação ambiente dentro de seu próprio escopo.

  • Não participar de uma transação. Como resultado, não há nenhuma transação ambiental.

Se o escopo for instanciado com Required e uma transação ambiente estiver presente, o escopo ingressará nessa transação. Se, por outro lado, não houver nenhuma transação ambiente, o escopo criará uma nova transação e se tornará o escopo raiz. Este é o valor padrão. Quando Required é usado, o código dentro do escopo não precisa comportar-se de forma diferente, quer seja o elemento raiz ou apenas esteja a integrar-se numa transação ambiente. Deve funcionar de forma idêntica em ambos os casos.

Se o escopo for instanciado com RequiresNew, ele será sempre o escopo raiz. Ele inicia uma nova transação, e sua transação se torna a nova transação ambiente dentro do escopo.

Se o escopo for instanciado com Suppress, ele nunca participará de uma transação, independentemente de uma transação ambiente estar presente. Um escopo instanciado com esse valor sempre tem null como transação ambiente.

As opções acima estão resumidas na tabela a seguir.

TransactionScopeOption Transação Ambiental O âmbito de aplicação abrange
Obrigatório Não Nova transação (será a raiz)
Requer um novo Não Nova transação (será a raiz)
Suprimir Não Sem transação
Obrigatório Sim Transação Ambiental
Requer um novo Sim Nova transação (será a raiz)
Suprimir Sim Sem transação

Quando um TransactionScope objeto ingressa em uma transação de ambiente existente, a eliminação do objeto de escopo pode não encerrar a transação, a menos que o escopo anule a transação. Se a transação de ambiente foi criada por um escopo raiz, apenas quando o escopo raiz é descartado, é que Commit será chamado na transação. Se a transação for criada manualmente, a transação termina quando é abortada ou comprometida pelo seu criador.

O exemplo a seguir mostra um objeto TransactionScope que cria três objetos de escopo aninhados, cada um instanciado com um valor TransactionScopeOption diferente.

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))
    {
        //...  
    }
}

O exemplo mostra um bloco de código sem qualquer transação ambiente, criando um novo escopo (scope1) com Required. O escopo scope1 é um escopo raiz, pois cria uma nova transação (Transação A) e torna a Transação A a transação ambiente. Scope1 Em seguida, cria mais três objetos, cada um com um valor diferente TransactionScopeOption . Por exemplo, scope2 é criado com Required, e como há uma transação ambiente, ele se junta à primeira transação criada pelo scope1. Observe que scope3 é o escopo raiz de uma nova transação e que scope4 não tem transação ambiente.

Embora o valor padrão e mais comumente usado de TransactionScopeOption é Required, cada um dos outros valores tem sua finalidade exclusiva.

Código não transacional dentro de um escopo de transação

Suppress é útil quando pretende preservar as operações executadas pelo código e não deseja anular a transação dentro do ambiente se as operações falharem. Por exemplo, quando pretende executar operações de registo ou auditoria, ou quando quer publicar eventos para subscritores, independentemente de a sua transação em contexto confirmar ou abortar. Esse valor permite que você tenha uma seção de código não transacional dentro de um escopo de transação, conforme mostrado no exemplo a seguir.

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
}

Votação dentro de um escopo aninhado

Embora um escopo aninhado possa ingressar na transação de ambiente do escopo raiz, chamar Complete dentro do escopo aninhado não afeta a transação do escopo raiz. A transação só será confirmada se todos os escopos, desde o escopo raiz até o último escopo aninhado, votarem pela confirmação da transação. Não chamar Complete dentro de um escopo aninhado afetará o escopo raiz, pois a transação ambiente será imediatamente anulada.

Definindo o tempo limite de TransactionScope

Alguns dos construtores sobrecarregados de TransactionScope aceitam um valor do tipo TimeSpan, usado para controlar o timeout da transação. Um tempo limite definido como zero significa um tempo limite infinito. O tempo limite infinito é útil principalmente para depuração; quando o utilizador deseja isolar um problema na sua lógica de negócios, passando passo a passo pelo código, e não quer que a transação que está a depurar atinja o tempo limite enquanto tenta localizar o problema. Tenha muito cuidado ao usar o valor de tempo limite infinito em todos os outros casos, pois ele substitui as proteções contra bloqueios de transações.

Normalmente, você define o TransactionScope tempo limite para valores diferentes do padrão em dois casos. A primeira é durante o desenvolvimento, quando você deseja testar a maneira como seu aplicativo lida com transações abortadas. Ao definir o tempo limite para um pequeno valor (como um milissegundo), você faz com que sua transação falhe e, assim, pode observar seu código de tratamento de erros. O segundo caso em que você define o valor como menor do que o tempo limite padrão é quando você acredita que o escopo está envolvido na contenção de recursos, resultando em bloqueios. Nesse caso, você deseja abortar a transação o mais rápido possível e não esperar que o tempo limite padrão expire.

Quando um escopo ingressa em uma transação ambiente, mas especifica um tempo limite menor do que aquele para o qual a transação ambiente está definida, o novo tempo limite mais curto é imposto ao TransactionScope objeto e o escopo deve terminar dentro do tempo aninhado especificado ou a transação é automaticamente anulada. Se o tempo limite do escopo aninhado for maior do que o da transação ambiental, ele não terá efeito.

Definindo o nível de isolamento do TransactionScope

Alguns dos construtores sobrecarregados de TransactionScope aceitam uma estrutura do tipo TransactionOptions para especificar um nível de isolamento, além de um valor de tempo limite. Por padrão, a transação é executada com o nível de isolamento definido como Serializable. Selecionar um nível de isolamento diferente de Serializable é comum para sistemas com uso intensivo de leitura. Isso requer uma sólida compreensão da teoria de processamento de transações e da semântica da transação em si, as questões de simultaneidade envolvidas e as consequências para a consistência do sistema.

Além disso, nem todos os gerentes de recursos suportam todos os níveis de isolamento, e eles podem optar por participar da transação em um nível mais alto do que o configurado.

Todos os níveis de isolamento além de Serializable correm risco de inconsistências causadas por outras transações que acessam as mesmas informações. A diferença entre os diferentes níveis de isolamento está na forma como os bloqueios de leitura e escrita são usados. Um bloqueio pode ser mantido somente quando a transação acessa os dados no gerenciador de recursos, ou pode ser mantido até que a transação seja confirmada ou abortada. O primeiro é melhor para o rendimento, o segundo para a consistência. Os dois tipos de bloqueios e os dois tipos de operações (leitura/gravação) fornecem quatro níveis básicos de isolamento. Consulte IsolationLevel para obter mais informações.

Ao usar objetos aninhados TransactionScope, todos os escopos aninhados devem ser configurados para usar exatamente o mesmo nível de isolamento se quiserem ingressar na transação ambiente. Se um objeto aninhado TransactionScope tentar participar na transação ambiente, mas especificar um nível de isolamento diferente, um ArgumentException será gerado.

Interoperabilidade com COM+

Quando você cria uma nova TransactionScope instância, você pode usar a EnterpriseServicesInteropOption enumeração em um dos construtores para especificar como interagir com COM+. Para obter mais informações sobre isso, consulte Interoperabilidade com serviços corporativos e transações COM+.

Ver também