Dans quelles circonstances un SqlConnection est-il automatiquement enrôlé dans une transaction TransactionScope ambiante?

201

Qu'est-ce que cela signifie pour une SqlConnection d'être «enrôlé» dans une transaction? Cela signifie-t-il simplement que les commandes que j'exécute sur la connexion participeront à la transaction?

Si tel est le cas, dans quelles circonstances un SqlConnection est-il automatiquement enrôlé dans une transaction TransactionScope ambiante?

Voir les questions dans les commentaires de code. Je suppose que la réponse à chaque question suit chaque question entre parenthèses.

Scénario 1: Ouverture de connexions DANS UNE étendue de transaction

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?)
}

Scénario 2: Utilisation de connexions à l'intérieur d'une étendue de transaction qui a été ouverte à l'extérieur de celle-ci

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

Réponses:

188

J'ai fait quelques tests depuis que j'ai posé cette question et j'ai trouvé la plupart sinon toutes les réponses par moi-même, car personne d'autre n'a répondu. Veuillez me faire savoir si j'ai raté quelque chose.

Q1. Oui, sauf si "enlist = false" est spécifié dans la chaîne de connexion. Le pool de connexions trouve une connexion utilisable. Une connexion utilisable est une connexion qui n'est pas inscrite dans une transaction ou une connexion qui est inscrite dans la même transaction.

Q2. La deuxième connexion est une connexion indépendante, qui participe à la même transaction. Je ne suis pas sûr de l'interaction des commandes sur ces deux connexions, car elles s'exécutent sur la même base de données, mais je pense que des erreurs peuvent se produire si des commandes sont émises sur les deux en même temps: des erreurs comme "Contexte de transaction utilisé par une autre session "

Q3. Oui, il est escaladé en une transaction distribuée, donc l'inscription de plusieurs connexions, même avec la même chaîne de connexion, la transforme en transaction distribuée, ce qui peut être confirmé en recherchant un GUID non nul à Transaction.Current.TransactionInformation .DistributedIdentifier. * Mise à jour: j'ai lu quelque part que cela est corrigé dans SQL Server 2008, de sorte que MSDTC n'est pas utilisé lorsque la même chaîne de connexion est utilisée pour les deux connexions (tant que les deux connexions ne sont pas ouvertes en même temps). Cela vous permet d'ouvrir une connexion et de la fermer plusieurs fois dans une transaction, ce qui pourrait faire un meilleur usage du pool de connexions en ouvrant les connexions le plus tard possible et en les fermant le plus tôt possible.

Q4. Non. Une connexion ouverte alors qu'aucune étendue de transaction n'était active, ne sera pas automatiquement inscrite dans une étendue de transaction nouvellement créée.

Q5. Non. À moins que vous n'ouvriez une connexion dans la portée de la transaction ou que vous ne recrutiez une connexion existante dans la portée, il n'y a essentiellement AUCUNE TRANSACTION. Votre connexion doit être automatiquement ou manuellement inscrite dans la portée de la transaction pour que vos commandes participent à la transaction.

Q6. Oui, les commandes sur une connexion ne participant pas à une transaction sont validées telles qu'elles ont été émises, même si le code se trouve avoir été exécuté dans un bloc de portée de transaction qui a été annulé. Si la connexion n'est pas inscrite dans la portée de la transaction actuelle, elle ne participe pas à la transaction, donc la validation ou l'annulation de la transaction n'aura aucun effet sur les commandes émises sur une connexion non inscrite dans la portée de la transaction ... comme l'a découvert ce type . C'est très difficile à repérer à moins que vous ne compreniez le processus d'inscription automatique: il ne se produit que lorsqu'une connexion est ouverte dans une étendue de transaction active.

Q7. Oui. Une connexion existante peut être explicitement inscrite dans la portée de transaction actuelle en appelant EnlistTransaction (Transaction.Current). Vous pouvez également inscrire une connexion sur un thread distinct dans la transaction en utilisant une DependentTransaction, mais comme auparavant, je ne sais pas comment deux connexions impliquées dans la même transaction avec la même base de données peuvent interagir ... et des erreurs peuvent se produire, et bien sûr, la deuxième connexion enregistrée provoque l'escalade de la transaction en une transaction distribuée.

Q8. Une erreur peut être levée. Si TransactionScopeOption.Required a été utilisé et que la connexion était déjà inscrite dans une transaction de portée de transaction, il n'y a pas d'erreur; en fait, aucune nouvelle transaction n'est créée pour la portée et le nombre de transactions (@@ trancount) n'augmente pas. Si, cependant, vous utilisez TransactionScopeOption.RequiresNew, vous obtenez un message d'erreur utile lors de la tentative d'inscription de la connexion dans la nouvelle transaction de portée de transaction: «La connexion a actuellement une transaction inscrite. Terminez la transaction en cours et réessayez.» Et oui, si vous terminez la transaction à laquelle la connexion est inscrite, vous pouvez sécuriser la connexion dans une nouvelle transaction. Mise à jour: si vous avez précédemment appelé BeginTransaction sur la connexion, une erreur légèrement différente est émise lorsque vous essayez de vous inscrire dans une nouvelle transaction de portée de transaction: "Impossible de s'inscrire dans la transaction car une transaction locale est en cours sur la connexion. Terminez la transaction locale et recommencez." D'un autre côté, vous pouvez appeler en toute sécurité BeginTransaction sur SqlConnection pendant qu'il est inscrit dans une transaction de portée de transaction, et cela augmentera en fait @@ trancount de un, contrairement à l'utilisation de l'option requise d'une portée de transaction imbriquée, qui ne le fait pas augmenter. Fait intéressant, si vous continuez à créer une autre étendue de transaction imbriquée avec l'option Obligatoire, vous n'obtiendrez pas d'erreur,

Q9. Oui. Les commandes participent à la transaction dans laquelle la connexion est inscrite, quelle que soit la portée de la transaction active dans le code C #.

Triynko
la source
11
Après avoir écrit la réponse à Q8, je réalise que ce truc commence à paraître aussi compliqué que les règles de Magic: The Gathering! Sauf que c'est pire, car la documentation de TransactionScope n'explique rien de tout cela.
Triynko
Pour Q3, ouvrez-vous deux connexions en même temps en utilisant la même chaîne de connexion? Si c'est le cas, alors ce sera une transaction distribuée (même avec SQL Server 2008)
Randy prend en charge Monica
2
Non, je modifie le message pour clarifier. Ma compréhension est que le fait d'avoir deux connexions ouvertes en même temps provoquera toujours une transaction distribuée, quelle que soit la version de SQL Server. Avant SQL 2008, l'ouverture d'une seule connexion à la fois, avec la même chaîne de connexion, provoquait toujours un DT, mais avec SQL 2008, l'ouverture d'une connexion à la fois (sans jamais en ouvrir deux à la fois) avec la même chaîne de connexion ne provoquait pas de DT
Triynko
1
Pour clarifier votre réponse pour Q2, les deux commandes devraient fonctionner correctement si elles sont exécutées séquentiellement sur le même thread.
Jared Moore
2
Sur le problème de promotion Q3 pour des chaînes de connexion identiques dans SQL 2008, voici la citation MSDN: msdn.microsoft.com/en-us/library/ms172070(v=vs.90).aspx
pseudocoder
19

Beau travail Triynko, vos réponses me semblent toutes très précises et complètes. Je voudrais souligner d'autres choses:

(1) Inscription manuelle

Dans votre code ci-dessus, vous affichez (correctement) l'inscription manuelle comme ceci:

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

Cependant, il est également possible de le faire comme ceci, en utilisant Enlist = false dans la chaîne de connexion.

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

Il y a autre chose à noter ici. Lorsque conn2 est ouvert, le code du pool de connexions ne sait pas que vous souhaitez l'enrôler ultérieurement dans la même transaction que conn1, ce qui signifie que conn2 reçoit une connexion interne différente de conn1. Ensuite, lorsque conn2 est inscrit, il y a maintenant 2 connexions inscrites, de sorte que la transaction doit être promue en MSDTC. Cette promotion ne peut être évitée qu'en utilisant l'inscription automatique.

(2) Avant .Net 4.0, je recommande vivement de définir "Transaction Binding = Explicit Unbind" dans la chaîne de connexion . Ce problème est résolu dans .Net 4.0, rendant Explicit Unbind totalement inutile.

(3) Rouler le vôtre CommittableTransactionet le régler Transaction.Currentest essentiellement la même chose que ce qui se TransactionScopepasse. C'est rarement réellement utile, juste pour info.

(4) Transaction.Current est statique. Cela signifie que ce Transaction.Currentparamètre est uniquement défini sur le thread qui a créé le fichier TransactionScope. Ainsi, plusieurs threads exécutant la même chose TransactionScope(éventuellement en utilisant Task) ne sont pas possibles.

Jared Moore
la source
Je viens de tester ce scénario, et il semble fonctionner comme vous le décrivez. En outre, même si vous utilisez l'inscription automatique, si vous appelez «SqlConnection.ClearAllPools ()» avant d'ouvrir la deuxième connexion, elle est ensuite convertie en une transaction distribuée.
Triynko
Si cela est vrai, alors il ne peut y avoir qu'une seule "vraie" connexion impliquée dans une transaction. La possibilité d'ouvrir, de fermer et de rouvrir une connexion inscrite dans une transaction TransactionScope sans escalader vers une transaction distribuée est alors vraiment une illusion créée par le pool de connexions , qui laisserait normalement la connexion supprimée ouverte et renverrait cette même connexion exacte si re -ouvert pour l'enrôlement automatique.
Triynko
Donc, ce que vous dites vraiment, c'est que si vous contournez le processus d'inscription automatique, puis lorsque vous allez rouvrir une nouvelle connexion dans une transaction de portée de transaction (TST), au lieu que le pool de connexions n'attrape la connexion correcte (celle d'origine enrôlé dans le TST), il saisit de manière tout à fait appropriée une toute nouvelle connexion, qui, lorsqu'il est enrôlé manuellement, provoque l'escalade du TST.
Triynko
Quoi qu'il en soit, c'est exactement ce que je faisais allusion dans ma réponse à Q1 lorsque j'ai mentionné qu'il est inscrit à moins que "Enlist = false" ne soit spécifié dans la chaîne de connexion, puis expliqué comment le pool trouve une connexion appropriée.
Triynko
En ce qui concerne le multi-threading, si vous visitez le lien dans ma réponse au Q2, vous verrez que même si Transaction.Current est unique à chaque thread, vous pouvez facilement acquérir la référence dans un thread et la transmettre à un autre thread; cependant, l'accès à un TST à partir de deux threads différents entraîne une erreur très spécifique "Contexte de transaction utilisé par une autre session". Pour multi-threader un TST, vous devez créer une DependantTransaction, mais à ce stade, il doit s'agir d'une transaction distribuée, car vous avez besoin d'une deuxième connexion indépendante pour exécuter réellement des commandes simultanées et MSDTC pour coordonner les deux.
Triynko
1

Une autre situation bizarre que nous avons vue est que si vous construisez un, EntityConnectionStringBuilderil va bouger TransactionScope.Currentet (nous pensons) vous engager dans la transaction. Nous avons observé cela dans le débogueur, où TransactionScope.Current« s current.TransactionInformation.internalTransactionmontre enlistmentCount == 1avant de construire, et par la enlistmentCount == 2suite.

Pour éviter cela, construisez-le à l'intérieur

using (new TransactionScope(TransactionScopeOption.Suppress))

et peut-être en dehors de la portée de votre opération (nous la construisions chaque fois que nous avions besoin d'une connexion).

Todd
la source