Em que circunstâncias um SqlConnection é automaticamente inscrito em uma transação TransactionScope de ambiente?

201

O que significa um SqlConnection ser "inscrito" em uma transação? Significa simplesmente que os comandos executados na conexão participarão da transação?

Em caso afirmativo, em que circunstâncias um SqlConnection é automaticamente inscrito em uma transação TransactionScope ambiente?

Veja as perguntas nos comentários do código. Meu palpite para a resposta de cada pergunta segue cada pergunta entre parênteses.

Cenário 1: Abrindo conexões DENTRO de um escopo de transação

using (TransactionScope scope = new TransactionScope())
using (SqlConnection conn = ConnectToDB())
{   
    // Q1: Is connection automatically enlisted in transaction? (Yes?)
    //
    // Q2: If I open (and run commands on) a second connection now,
    // with an identical connection string,
    // what, if any, is the relationship of this second connection to the first?
    //
    // Q3: Will this second connection's automatic enlistment
    // in the current transaction scope cause the transaction to be
    // escalated to a distributed transaction? (Yes?)
}

Cenário 2: Usando conexões DENTRO de um escopo de transação que foi aberto FORA dele

//Assume no ambient transaction active now
SqlConnection new_or_existing_connection = ConnectToDB(); //or passed in as method parameter
using (TransactionScope scope = new TransactionScope())
{
    // Connection was opened before transaction scope was created
    // Q4: If I start executing commands on the connection now,
    // will it automatically become enlisted in the current transaction scope? (No?)
    //
    // Q5: If not enlisted, will commands I execute on the connection now
    // participate in the ambient transaction? (No?)
    //
    // Q6: If commands on this connection are
    // not participating in the current transaction, will they be committed
    // even if rollback the current transaction scope? (Yes?)
    //
    // If my thoughts are correct, all of the above is disturbing,
    // because it would look like I'm executing commands
    // in a transaction scope, when in fact I'm not at all, 
    // until I do the following...
    //
    // Now enlisting existing connection in current transaction
    conn.EnlistTransaction( Transaction.Current );
    //
    // Q7: Does the above method explicitly enlist the pre-existing connection
    // in the current ambient transaction, so that commands I
    // execute on the connection now participate in the
    // ambient transaction? (Yes?)
    //
    // Q8: If the existing connection was already enlisted in a transaction
    // when I called the above method, what would happen?  Might an error be thrown? (Probably?)
    //
    // Q9: If the existing connection was already enlisted in a transaction
    // and I did NOT call the above method to enlist it, would any commands
    // I execute on it participate in it's existing transaction rather than
    // the current transaction scope. (Yes?)
}
Triynko
fonte

Respostas:

188

Eu fiz alguns testes desde que fiz essa pergunta e encontrei a maioria, senão todas as respostas por conta própria, já que ninguém mais respondeu. Por favor, deixe-me saber se eu perdi alguma coisa.

Q1 Sim, a menos que "enlist = false" seja especificado na cadeia de conexão. O conjunto de conexões encontra uma conexão utilizável. Uma conexão utilizável é aquela que não está inscrita em uma transação ou que está inscrita na mesma transação.

Q2 A segunda conexão é uma conexão independente, que participa da mesma transação. Não tenho certeza sobre a interação dos comandos nessas duas conexões, uma vez que estão executando no mesmo banco de dados, mas acho que podem ocorrer erros se os comandos forem emitidos nos dois ao mesmo tempo: erros como "Contexto da transação em uso por outra sessão "

Q3 Sim, ele é escalado para uma transação distribuída, portanto, a inscrição de mais de uma conexão, mesmo com a mesma cadeia de conexão, faz com que ela se torne uma transação distribuída, o que pode ser confirmado pela verificação de um GUID não nulo em Transaction.Current.TransactionInformation .DistributedIdentifier. * Atualização: Li em algum lugar que isso foi corrigido no SQL Server 2008, para que o MSDTC não seja usado quando a mesma cadeia de conexão for usada nas duas conexões (desde que as duas conexões não estejam abertas ao mesmo tempo). Isso permite abrir uma conexão e fechá-la várias vezes em uma transação, o que poderia fazer melhor uso do conjunto de conexões, abrindo as conexões o mais tarde possível e fechando-as o mais rápido possível.

Q4. Não. Uma conexão aberta quando nenhum escopo de transação estava ativo não será automaticamente inscrita em um escopo de transação recém-criado.

Q5 Não. A menos que você abra uma conexão no escopo da transação ou inscreva uma conexão existente no escopo, basicamente não haverá TRANSAÇÃO. Sua conexão deve ser inscrita automaticamente ou manualmente no escopo da transação para que seus comandos participem da transação.

Q6 Sim, os comandos em uma conexão que não participa de uma transação são confirmados como emitidos, mesmo que o código tenha sido executado em um bloco de escopo de transação que foi revertido. Se a conexão não estiver inscrita no escopo da transação atual, ela não estará participando da transação, portanto, confirmar ou retroceder a transação não terá efeito sobre os comandos emitidos em uma conexão não inscrita no escopo da transação ... como esse cara descobriu . É muito difícil identificá-lo, a menos que você entenda o processo de inscrição automática: ele ocorre apenas quando uma conexão é aberta dentro de um escopo de transação ativo.

Q7 Sim. Uma conexão existente pode ser explicitamente inscrita no escopo da transação atual chamando EnlistTransaction (Transaction.Current). Você também pode inscrever uma conexão em um thread separado na transação usando uma DependentTransaction, mas, como antes, não tenho certeza de como duas conexões envolvidas na mesma transação no mesmo banco de dados podem interagir ... e podem ocorrer erros, e é claro que a segunda conexão alistada faz com que a transação seja escalada para uma transação distribuída.

Q8 Um erro pode ser gerado. Se TransactionScopeOption.Required foi usado, e a conexão já estava registrada em uma transação de escopo de transação, não há erro; de fato, não há nova transação criada para o escopo, e a contagem de transações (@@ trancount) não aumenta. No entanto, se você usar TransactionScopeOption.RequiresNew, receberá uma mensagem de erro útil ao tentar inscrever a conexão na nova transação de escopo da transação: "A conexão atualmente possui uma transação registrada. Conclua a transação atual e tente novamente." E sim, se você concluir a transação na qual a conexão está inscrita, poderá inscrevê-la com segurança em uma nova transação. Atualização: se você já chamou BeginTransaction na conexão, um erro ligeiramente diferente é gerado quando você tenta se inscrever em uma nova transação de escopo de transação: "Não é possível se inscrever na transação porque uma transação local está em andamento na conexão. Conclua a transação local e tente novamente ". Por outro lado, você pode chamar BeginTransaction com segurança no SqlConnection enquanto estiver inscrito em uma transação de escopo de transação, e isso realmente aumentará @@ trancount em um, ao contrário do uso da opção Obrigatório de um escopo de transação aninhada, que não faz com que aumentar. Curiosamente, se você criar outro escopo de transação aninhada com a opção Obrigatório, não receberá um erro,

Q9 Sim. Os comandos participam de qualquer transação na qual a conexão está inscrita, independentemente do escopo da transação ativa no código C #.

Triynko
fonte
11
Depois de escrever a resposta para o Q8, percebo que essas coisas estão começando a parecer tão complicadas quanto as regras de Magic: The Gathering! Exceto que isso é pior, porque a documentação do TransactionScope não explica nada disso.
Triynko
Para o terceiro trimestre, você está abrindo duas conexões ao mesmo tempo usando a mesma cadeia de conexão? Nesse caso, será uma transação distribuída (mesmo com o SQL Server 2008)
Randy suporta Monica
2
Não. Eu edito a postagem para esclarecer. Meu entendimento é que ter duas conexões abertas ao mesmo tempo sempre causará uma transação distribuída, independentemente da versão do SQL Server. Antes do SQL 2008, abrir apenas uma conexão por vez, com a mesma cadeia de conexão, ainda causaria uma DT, mas com o SQL 2008, abrir uma conexão por vez (nunca tendo duas abertas ao mesmo tempo) com a mesma cadeia de conexão não causaria uma DT. DT
Triynko
1
Para esclarecer sua resposta para o Q2, os dois comandos devem funcionar bem se forem executados sequencialmente no mesmo encadeamento.
Jared Moore
2
Sobre a questão da promoção Q3 para cordas de ligação idênticas em SQL 2008, aqui é a citação MSDN: msdn.microsoft.com/en-us/library/ms172070(v=vs.90).aspx
pseudocoder
19

Bom trabalho Triynko, todas as suas respostas parecem bastante precisas e completas para mim. Gostaria de destacar outras coisas:

(1) Alistamento manual

No código acima, você (corretamente) mostra o alistamento manual como este:

using (SqlConnection conn = new SqlConnection(connStr))
{
    conn.Open();
    using (TransactionScope ts = new TransactionScope())
    {
        conn.EnlistTransaction(Transaction.Current);
    }
}

No entanto, também é possível fazer assim, usando Enlist = false na cadeia de conexão.

string connStr = "...; Enlist = false";
using (TransactionScope ts = new TransactionScope())
{
    using (SqlConnection conn1 = new SqlConnection(connStr))
    {
        conn1.Open();
        conn1.EnlistTransaction(Transaction.Current);
    }

    using (SqlConnection conn2 = new SqlConnection(connStr))
    {
        conn2.Open();
        conn2.EnlistTransaction(Transaction.Current);
    }
}

Há outra coisa a observar aqui. Quando o conn2 é aberto, o código do conjunto de conexões não sabe que você deseja incluí-lo posteriormente na mesma transação que o conn1, o que significa que o conn2 recebe uma conexão interna diferente da conn1. Em seguida, quando o conn2 é registrado, agora existem 2 conexões, portanto a transação deve ser promovida para o MSDTC. Esta promoção só pode ser evitada usando o alistamento automático.

(2) Antes do .Net 4.0, eu recomendo a configuração "Ligação de transação = Desvinculação explícita" na cadeia de conexão . Esse problema foi corrigido no .Net 4.0, tornando o Explicit Unbind totalmente desnecessário.

(3) Rolar o seu próprio CommittableTransactione definir o Transaction.Currentque é essencialmente a mesma coisa que o que TransactionScopefaz. Isso raramente é realmente útil, apenas para sua informação.

(4) Transaction.Current é estático da linha. Isso significa que Transaction.Currenté definido apenas no segmento que criou o arquivo TransactionScope. Portanto, vários threads executando o mesmo TransactionScope(possivelmente usando Task) não são possíveis.

Jared Moore
fonte
Acabei de testar esse cenário e parece funcionar como você descreve. Além disso, mesmo se você usar a inscrição automática, se chamar "SqlConnection.ClearAllPools ()" antes de abrir a segunda conexão, ela será encaminhada para uma transação distribuída.
Triynko 28/09
Se isso for verdade, só poderá haver uma única conexão "real" envolvida em uma transação. A capacidade de abrir, fechar e reabrir uma conexão alistada em uma transação TransactionScope sem escalar para uma transação distribuída é realmente uma ilusão criada pelo pool de conexões , que normalmente deixaria a conexão descartada aberta e retornaria a mesma conexão exata se re - aberto para alistamento automático.
Triynko 28/09
Então, o que você realmente está dizendo é que, se você contornar o processo de inscrição automática, quando abrir novamente uma nova conexão dentro de uma transação de escopo de transação (TST), em vez de o pool de conexões pegar a conexão correta (a originalmente alistado no TST), ele pega apropriadamente uma conexão completamente nova, que quando alistada manualmente, faz com que o TST seja escalado.
Triynko 28/09
De qualquer forma, é exatamente isso que eu estava sugerindo na minha resposta ao Q1 quando mencionei que ele estava inscrito, a menos que "Enlist = false" seja especificado na cadeia de conexão e depois falei sobre como o pool encontra uma conexão adequada.
Triynko 28/09
No que diz respeito à multi-segmentação, se você visitar o link na minha resposta à Q2, verá que, embora Transaction.Current seja exclusivo de cada segmento, você pode facilmente adquirir a referência em um segmento e passá-lo para outro segmento; no entanto, acessar um TST a partir de dois threads diferentes resulta em um erro muito específico "Contexto de transação em uso por outra sessão". Para multiencadear um TST, você deve criar uma DependantTransaction, mas nesse momento deve ser uma transação distribuída, porque você precisa de uma segunda conexão independente para realmente executar comandos simultâneos e o MSDTC para coordenar os dois.
Triynko 28/09
1

Uma outra situação bizarra que vimos é que, se você construir uma, EntityConnectionStringBuilderela estragará TransactionScope.Currente (pensamos) se alistar na transação. Observamos isso no depurador, onde TransactionScope.Currentos current.TransactionInformation.internalTransactionprogramas são mostrados enlistmentCount == 1antes da construção e enlistmentCount == 2depois.

Para evitar isso, construa-o dentro

using (new TransactionScope(TransactionScopeOption.Suppress))

e possivelmente fora do escopo de sua operação (estávamos construindo toda vez que precisávamos de uma conexão).

Todd
fonte