8 07 2015
Transações distribuídas com o TransactionScope
Ao trabalharmos com o desenvolvimento de aplicações de negócios, certamente um dia acabamos nos deparando com a necessidade de criarmos transações, principalmente para a execução de comandos no banco de dados. Se você cursa ou cursou alguma faculdade relacionada a desenvolvimento de software, provavelmente seu professor de banco de dados deu aquele exemplo de aplicação financeira em que queremos tirar dinheiro de uma conta para colocarmos em outra conta. Esse processo deve ser executado dentro de uma transação, pois, se algo de errado acontecer, as contas devem permanecer intactas. Mas, e se as contas estiverem em bancos de dados separados? Aí temos que utilizar transações distribuídas!
A classe que lida com transações distribuídas no .NET Framework é a TransactionScope. E esse será o tema do artigo de hoje.
O exemplo dado no início do artigo é um exemplo clássico de professores de banco de dados. Apesar de fazer todo o sentido, nos dias de hoje eu fico sempre imaginando como é que realmente funciona um sistema bancário por trás dos panos. O que acontece no sistema do banco quando transferimos dinheiro de uma conta X para uma conta Y? Certamente existe alguma boa razão para os bancos hesitarem migrar as suas infraestruturas que utilizam mainframe e COBOL desenvolvidas anos atrás. A complexidade deve ser gigantesca! E o código deve estar rodando há anos sem ter sido alterado.
Enfim, nós não precisamos de uma precisão tão grande assim para entendermos o conceito de transações. Vamos simplificar o máximo que pudermos para focarmos no conceito da criação de transações.
Transações de conexão
O primeiro passo do nosso exemplo será a utilização de transações em um único banco de dados, ou seja, uma transação que envolve uma única conexão. Para isso vamos imaginar que temos uma tabela que armazena os saldos das contas corrente. No nosso exemplo, essa tabela será extremamente simples, contendo somente duas colunas: NumeroConta e Saldo:
CREATE TABLE [Conta]( [NumeroConta] [varchar](50) NOT NULL, [Saldo] [numeric](18, 5) NULL, CONSTRAINT [PK_Conta] PRIMARY KEY CLUSTERED ( [NumeroConta] ASC ) ON [PRIMARY] ) ON [PRIMARY]
Vamos popular essa tabela com apenas duas contas: a conta número 12345, com saldo de R$ 5500; e a conta número 54321, mais pobre, com saldo de R$ 890:
INSERT INTO [Conta] ([NumeroConta] ,[Saldo]) VALUES (12345, 5500); INSERT INTO [Conta] ([NumeroConta] ,[Saldo]) VALUES (54321, 890);
Feito isso, vamos criar um projeto do tipo “Console Application” para fazermos a movimentação de valores entre as contas. Se simplesmente quisermos tirar R$55,55 da conta 12345 para adicionarmos na conta 54321, sem a utilização de transações (totalmente errado – não faça dessa maneira!), o código ficaria parecido com este:
static void Main(string[] args) { using (var connection = new System.Data.SqlClient.SqlConnection(@"server=.\sqlexpress;database=exemplotransacao;trusted_connection=true;")) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = "update conta set saldo = saldo - 55.55 where numeroconta = 12345"; command.ExecuteNonQuery(); } using (var command = connection.CreateCommand()) { command.CommandText = "update conta set saldo = saldo + 55.55 where numeroconta = 54321"; command.ExecuteNonQuery(); } } }
Porém, pense bem na seguinte situação. Algum imprevisto acontece (como uma queda de energia ou uma indisponibilidade temporária do banco de dados) entre o primeiro e o segundo comando. Qual seria o resultado? O valor teria sido descontado da conta 12345, mas, o depósito na conta 54321 não teria acontecido. Se você fosse o dono da conta 12345 você ficaria muito nervoso, não é mesmo? Então, vamos consertar o exemplo para que ele seja mais tolerante a falhas:
using (var connection = new System.Data.SqlClient.SqlConnection(@"server=.\sqlexpress;database=exemplotransacao;trusted_connection=true;")) { connection.Open(); using (var transaction = connection.BeginTransaction()) { try { using (var command = connection.CreateCommand()) { command.Transaction = transaction; command.CommandText = "update conta set saldo = saldo - 55.55 where numeroconta = 12345"; command.ExecuteNonQuery(); } using (var command = connection.CreateCommand()) { command.Transaction = transaction; command.CommandText = "update conta set saldo = saldo + 55.55 where numeroconta = 54321"; command.ExecuteNonQuery(); } transaction.Commit(); } catch { transaction.Rollback(); } } }
Notem que adicionamos a criação de uma transação (método BeginTransaction da conexão) e um bloco “try-catch” onde o nosso código com as atualizações será executado. Após a execução de ambos os comandos com sucesso, nós utilizamos o método Commit da transação, e só então os comandos de atualização serão realmente efetivados no banco de dados (até esse ponto ambos os registros que representam as contas estavam bloqueados pelo banco de dados, ou seja, nenhum outro sistema poderia acessá-los nesse meio tempo). Caso algo dê errado nesse processo, no bloco “catch” temos a chamada do método RollBack, que fará com que as alterações feitas nos registros sejam descartadas.
Dessa forma, mesmo se alguma falha grave aconteça entre a primeira e a segunda atualização, o banco de dados continuará intacto. O banco de dados contará com os novos valores somente quando as duas atualizações forem realizadas com sucesso. Agora ficamos mais tranquilos, certo?
Transações distribuídas
O exemplo que acompanhamos acima é muito útil para entendermos transações que ocorrem no escopo de uma única base de dados. Porém, como ficaria esse exemplo se uma conta estivesse em um banco de dados e a outra conta estivesse em outro banco de dados? Nesse caso, temos que utilizar o conceito de transações distribuídas, implementadas no .NET Framework através da classe TransactionScope.
Para exemplificarmos esse cenário, vamos criar um outro banco de dados com a mesma tabela (pode ser no mesmo servidor ou até mesmo em servidores diferentes). Digamos que esse banco de dados representa a conta corrente secreta que o usuário supostamente teria em algum paraíso fiscal, hehe. O número da conta seria 98765 e o saldo R$100000 (cem mil reais):
INSERT INTO [Conta] ([NumeroConta] ,[Saldo]) VALUES (98765, 100000);
Agora que já temos o nosso segundo banco de dados, vamos voltar para o código da aplicação. Primeiramente temos que adicionar uma referência ao assembly “System.Transactions“, uma vez que a classe TransactionScope se encontra dentro desse assembly. Feito isso, partimos para a utilização dessa classe, que é extremamente simples. Basicamente temos que criar um bloco “using” onde instanciamos um TransactionScope e, dentro desse bloco “using“, realizamos as operações nos dois bancos de dados e chamamos o método Complete após termos atualizado os dois bancos de dados:
using (var transactionScope = new System.Transactions.TransactionScope()) { using (var connection = new System.Data.SqlClient.SqlConnection(@"Server=.\sqlexpress;Database=ExemploTransacao;Trusted_Connection=True;")) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = "UPDATE Conta SET Saldo = Saldo - 55.55 WHERE NumeroConta = 12345"; command.ExecuteNonQuery(); } } using (var connection = new System.Data.SqlClient.SqlConnection(@"Server=.\sqlexpress;Database=ExemploTransacao2;Trusted_Connection=True;")) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = "UPDATE Conta SET Saldo = Saldo + 55.55 WHERE NumeroConta = 98765"; command.ExecuteNonQuery(); } } transactionScope.Complete(); }
Porém, ao tentarmos executar esse código, receberemos uma exceção:
Additional information: MSDTC on server ‘ANDRENOTEDELL\SQLEXPRESS’ is unavailable.
Isso acontece, pois, as transações distribuídas são gerenciadas pelo tal “MSDTC” (Microsoft Distributed Transaction Coordinator) e ele vem desabilitado por padrão quando instalamos o Windows (afinal de contas, não é todo mundo que vai precisar rodar transações distribuídas no seu computador). Para habilitarmos o MSDTC, temos que abrir as ferramentas administrativas e acessarmos o item “Component Services“. Dentro da janela de “Component Services“, expanda o nó “Computers“, depois “My Computer“, clique em “Distributed Transaction Coordinator” e depois com o botão direito em “Local DTC“, abra a sua janela de propriedades:
Na janela de propriedades do “Local DTC“, temos que habilitar os itens “Network DTC Access“, “Allow Inbound“, “Allow Outbound” e “No Authentication Required” (ou escolher a forma de autenticação que você deseja utilizar, caso o seu cenário necessite de autenticação para o DTC):
Feito isso, ao clicarmos “OK“, o Windows informará que tem que reiniciar o serviço de DTC para que as alterações tenham efeito. Confirme o diálogo e pronto! O MSDTC está habilitado. Vale a pena ressaltar que, ao trabalharmos com bancos de dados em servidores diferentes, temos que habilitar o MSDTC em todos os servidores que participarão da transação distribuída.
Uma vez configurado o MSDTC, ao executarmos novamente o nosso exemplo, ele funcionará sem erros.
Você consegue imaginar o quão poderoso o TransactionScope acaba sendo? Podemos iniciar uma transação entre bancos de dados que estão há milhares de quilômetros de distância e garantir que as operações só serão efetivadas caso tudo tenha ocorrido com sucesso. Parece mágica, não?
Além de controlar transações com bancos de dados, o TransactionScope também consegue controlar transações de praticamente qualquer coisa, como, por exemplo, o sistema de arquivos. Digamos que em uma das etapas de uma funcionalidade você crie um arquivo e você quer que esse arquivo seja excluído caso algum erro aconteça nos próximos passos. Isso também é possível de se fazer com o TransactionScope, só que não por padrão, como é o caso das transações em bancos de dados. Se você quiser maiores informações e outros cenários da utilização do TransactionScope (inclusive o cenário da transação envolvendo arquivos em disco), confira este artigo no CodeProject que mostra tudo sobre TransactionScope.
Edit: Depois de publicar esse artigo no Facebook, o Dennes Torres comentou que as configurações padrão do TransactionScope não são tão boas, e que acabam causando muitos deadlocks. A sugestão dele é que alteremos o IsolationLevel para ReadCommited. Confira maiores detalhes no site dele: Cuidado ao utilizar o TransactionScope no .NET.
Concluindo
Muita gente passa anos desenvolvendo sistemas sem saber o que é uma transação. Nem todo mundo que trabalha com desenvolvimento de software passa por uma educação formal e, mesmo que passe, nem toda instituição de ensino se importa em ensinar tópicos importantes que utilizamos no mercado de trabalho. Eu mesmo não me lembro de ter aprendido sobre transações na universidade.
Não importa se você nunca tinha ouvido falar sobre transações até hoje. O que importa é que, agora que você aprendeu o conceito, sempre que for realizar alguma série de comandos críticos no banco de dados, envolva-os dentro de uma transação. Dessa forma você deixa o seu sistema mais robusto e menos propício a erros. E isso vale tanto para sistemas que utilizam somente um banco de dados (com transações de conexão) quanto para sistemas que utilizam múltiplos bancos de dados (com transações distribuídas).
É isso aí. Espero que esse artigo tenha trazido algo de novo para você. Caso você queira ficar por dentro de todos os novos artigos publicados aqui no meu site, além de ficar sabendo em primeira mão sobre os tópicos de artigos futuros, sugerir temas para próximos artigos e receber dicas extras que eu só compartilho por e-mail, assine a minha newsletter clicando aqui ou utilizando o formulário logo abaixo.
Até a próxima!
André Lima
Image by Ken Teegardin used under Creative Commons
https://www.flickr.com/photos/teegardin/5913014568/
Como eu consegui me mudar para a Alemanha? Dica rápida: Tirando screenshots no Microsoft Surface
Muito esclarecedor seu post, bem direto e com muita clareza. Gostaria de saber como faço esse processo utilizando o Entity Framework ao invés do ADO.Net “puro” como no seu post. Teria alguma dica de como fazer? Obrigado
Olá Reinaldo, obrigado pelo comentário!
Sinceramente, eu não tenho muita experiência com transações no Entity Framework, mas, dando uma pesquisada aqui, parece que quando você chama um SaveChanges no Entity Framework, ele já envolve todos os comandos necessários em uma transação..
Aparentemente é possível também criar transações manualmente se você quiser.. Dê uma olhada neste link.. Pode ser que te ajude:
Working with Transactions (EF6 Onwards)
Abraço!
André Lima
Olá. Estou usando o TransactionScope no projeto da empresa e apenas no ambiente de QA em algumas situações específicas dentro da transação está sendo disparada uma exception Transaction has abort. Conforme mencionei ocorre apenas no ambiente de QA nas máquinas dos desenvolvedores funciona perfeitamente. Tem alguma idéia do que possa ser isso? Desde já agradeço. Abs
Olá Nilton!
Estranho hein.. Pelo que entendi, as transações funcionam, mas, em algumas situações específicas o TransactionScope dá esse erro, certo? Você não conseguiu detectar nenhuma particularidade nessa situação em que a transação está dando erro? O que de especial você está fazendo nesse caso? As transações falham sempre na mesma situação ou falham aleatoriamente? O ambiente de QA é utilizado somente para essa aplicação ou você tem outras aplicações rodando nele ao mesmo tempo (se tiver outras aplicações, pode ser que elas estejam interferindo)..
Esse tipo de investigação é bem chato.. Normalmente dá um trabalhão descobrir o que é que está fazendo a transação falhar.. Você já deu uma olhada no log de eventos do Windows? Às vezes você consegue encontrar informações mais detalhadas lá..
De qualquer forma, se você conseguir resolver esse problema, volta aqui depois e fala pra gente o que era, OK?
Abraço!
André Lima
Olá André,
O TransactionScope falhava em uma mesma situação apenas em nosso ambiente de QA, nas máquians dos desenvolvedores tudo funcionava perfeitamente. O ambiente de QA é utilizado apenas para essa aplicação, porém é dividido em 2 máquinas. A princípio desconfiei que o problema poderia ocorrer pelo fato de acessarmos 2 bancos de dados diferentes na mesma transação, mas logo descartei pois fazemos isso no ambiente de desenvolvimento. Bom, não consegui identificar exatamente a causa do erro e o que me restou agora é desconfiar da aplicação rodar em 2 máquinas diferentes e do sistema operacional que é diferente entre os ambientes de QA e Desenv, repito é apenas uma desconfiança. No entanto conseguimos resolver o problema substituindo o TransactionScope pelo Database.BeginTransaction.
Olá Nilton!
Estranho hein.. Não tenho ideia do que possa estar acontecendo.. Já utilizei o TransactionScope com bancos de dados armazenado em servidores diferentes, porém, ambos os servidores tinham a mesma versão do Windows Server.. Pode ser que alguma coisa esteja faltando nas configurações do MSDTC em um dos servidores..
Mas, enfim, como você resolveu o problema com o BeginTransaction, bola pra frente! Qualquer coisa entre em contato novamente..
Abraço!
André Lima