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 gerenciador de recursos (como o SQL Server 2005) pode detetar a existência de uma transação de ambiente criada pelo escopo e se alistar automaticamente.
Criando 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 using
instrução está disponível em C# e no Visual Basic e funciona como um try
bloco ...finally
para garantir que o escopo seja descartado corretamente.
Quando você instancia TransactionScopeo , o gerenciador de transações determina em qual 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 Transaction propriedade static Transaction.Current da classe. 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 boa prática colocar a chamada como Complete a última declaração do using
bloco.
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 Complete método, você não pode mais acessar a transação de ambiente usando a Current propriedade, e tentar fazer isso resultará em uma exceção sendo lançada.
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 gerenciador de transações chama os gerentes de recursos e os informa para confirmar ou reverter, com base em se o Complete método foi chamado no TransactionScope objeto.
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 chamando um método que usa um TransactionScope de dentro de um método que usa seu próprio escopo, como é o caso do RootMethod
método 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:
Junte-se à transação de ambiente 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 de 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 predefinido. Quando Required é usado, o código dentro do escopo não precisa se comportar de forma diferente, seja a raiz ou apenas a junção da 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 participa em |
---|---|---|
Obrigatório | Não | Nova transação (será a raiz) |
Requer novo | Não | Nova transação (será a raiz) |
Suprimir | Não | Sem transação |
Necessário | Sim | Transação Ambiental |
Requer 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, somente quando o escopo raiz é descartado, é Commit chamado na transação. Se a transação foi criada manualmente, a transação termina quando é abortada ou confirmada pelo seu criador.
O exemplo a seguir mostra um TransactionScope objeto que cria três objetos de escopo aninhados, cada um instanciado com um valor diferente 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))
{
//...
}
}
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 você deseja preservar as operações executadas pela seção Código e não deseja anular a transação de ambiente se as operações falharem. Por exemplo, quando você deseja executar operações de log ou auditoria, ou quando deseja publicar eventos para assinantes, independentemente de sua transação ambiente 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 o escopo aninhado não tem efeito sobre o escopo raiz. A transação só será confirmada se todos os escopos, desde o escopo raiz até o último escopo aninhado, votarem para confirmar a transação. Não chamar Complete um escopo aninhado afetará o escopo raiz, pois a transação de ambiente será imediatamente anulada.
Definindo o tempo limite de TransactionScope
Alguns dos construtores sobrecarregados de TransactionScope aceitam um valor do tipo TimeSpan, que é usado para controlar o tempo limite da transação. Um tempo limite definido como zero significa um tempo limite infinito. O tempo limite infinito é útil principalmente para depuração, quando você deseja isolar um problema em sua lógica de negócios passando pelo código, e você não quer que a transação que você 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 do normalmente Serializable usado 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.
Além disso, todos os níveis Serializable de isolamento são suscetíveis a inconsistências resultantes de 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 de ambiente. Se um objeto aninhado TransactionScope tentar unir a transação de ambiente, mas especificar um nível de isolamento diferente, um ArgumentException será lançado.
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+.