Unter welchen Umständen wird eine SqlConnection automatisch in eine Umgebungs-TransactionScope-Transaktion aufgenommen?

201

Was bedeutet es für eine SqlConnection, in eine Transaktion "eingetragen" zu werden? Bedeutet dies einfach, dass Befehle, die ich für die Verbindung ausführe, an der Transaktion teilnehmen?

Wenn ja, unter welchen Umständen wird eine SqlConnection automatisch in eine Umgebungs-TransactionScope-Transaktion aufgenommen?

Siehe Fragen in Codekommentaren. Meine Vermutung zur Antwort jeder Frage folgt jeder Frage in Klammern.

Szenario 1: Öffnen von Verbindungen INNERHALB eines Transaktionsbereichs

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

Szenario 2: Verwenden von Verbindungen INNERHALB eines Transaktionsbereichs, der AUSSERHALB davon geöffnet wurde

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

Antworten:

188

Ich habe einige Tests durchgeführt, seit ich diese Frage gestellt habe, und die meisten, wenn nicht alle Antworten selbst gefunden, da sonst niemand geantwortet hat. Bitte lassen Sie mich wissen, wenn ich etwas verpasst habe.

Q1. Ja, es sei denn, in der Verbindungszeichenfolge ist "enlist = false" angegeben. Der Verbindungspool findet eine verwendbare Verbindung. Eine verwendbare Verbindung ist eine Verbindung, die nicht in einer Transaktion eingetragen ist, oder eine Verbindung, die in derselben Transaktion eingetragen ist.

Q2. Die zweite Verbindung ist eine unabhängige Verbindung, die an derselben Transaktion teilnimmt. Ich bin mir nicht sicher über die Interaktion von Befehlen auf diesen beiden Verbindungen, da sie für dieselbe Datenbank ausgeführt werden. Ich denke jedoch, dass Fehler auftreten können, wenn Befehle für beide gleichzeitig ausgegeben werden: Fehler wie "Transaktionskontext wird von verwendet." eine andere Sitzung "

Q3. Ja, es wird zu einer verteilten Transaktion eskaliert. Wenn Sie also mehr als eine Verbindung mit derselben Verbindungszeichenfolge eintragen, wird daraus eine verteilte Transaktion, die durch Überprüfen auf eine Nicht-Null-GUID bei Transaction.Current.TransactionInformation bestätigt werden kann .DistributedIdentifier. * Update: Ich habe irgendwo gelesen, dass dies in SQL Server 2008 behoben ist, sodass MSDTC nicht verwendet wird, wenn für beide Verbindungen dieselbe Verbindungszeichenfolge verwendet wird (solange nicht beide Verbindungen gleichzeitig geöffnet sind). Auf diese Weise können Sie eine Verbindung innerhalb einer Transaktion mehrmals öffnen und schließen. Dadurch kann der Verbindungspool besser genutzt werden, indem Verbindungen so spät wie möglich geöffnet und so schnell wie möglich geschlossen werden.

Q4. Nein. Eine Verbindung, die geöffnet wurde, als kein Transaktionsbereich aktiv war, wird nicht automatisch in einen neu erstellten Transaktionsbereich aufgenommen.

Q5. Nein. Sofern Sie keine Verbindung im Transaktionsbereich öffnen oder eine vorhandene Verbindung im Bereich registrieren, gibt es grundsätzlich KEINE TRANSAKTION. Ihre Verbindung muss automatisch oder manuell in den Transaktionsbereich aufgenommen werden, damit Ihre Befehle an der Transaktion teilnehmen können.

Q6. Ja, Befehle für eine Verbindung, die nicht an einer Transaktion teilnimmt, werden als ausgegeben festgeschrieben, obwohl der Code zufällig in einem Transaktionsbereichsblock ausgeführt wurde, der zurückgesetzt wurde. Wenn die Verbindung nicht im aktuellen Transaktionsbereich eingetragen ist, nimmt sie nicht an der Transaktion teil. Das Festschreiben oder Zurücksetzen der Transaktion hat also keine Auswirkungen auf Befehle, die für eine Verbindung ausgegeben werden, die nicht im Transaktionsbereich eingetragen ist ... wie dieser Typ herausgefunden hat . Dies ist nur schwer zu erkennen, wenn Sie den automatischen Registrierungsprozess verstehen: Er tritt nur auf, wenn eine Verbindung innerhalb eines aktiven Transaktionsbereichs geöffnet wird .

Q7. Ja. Eine vorhandene Verbindung kann durch Aufrufen von EnlistTransaction (Transaction.Current) explizit in den aktuellen Transaktionsbereich aufgenommen werden. Sie können eine Verbindung auch in einem separaten Thread in der Transaktion mithilfe einer DependentTransaction registrieren. Wie zuvor bin ich mir jedoch nicht sicher, wie zwei Verbindungen, die an derselben Transaktion mit derselben Datenbank beteiligt sind, interagieren können ... und Fehler auftreten können Natürlich bewirkt die zweite eingetragene Verbindung, dass die Transaktion zu einer verteilten Transaktion eskaliert.

Q8. Möglicherweise wird ein Fehler ausgegeben. Wenn TransactionScopeOption.Required verwendet wurde und die Verbindung bereits in einer Transaktion mit Transaktionsbereich eingetragen war, liegt kein Fehler vor. Tatsächlich wurde keine neue Transaktion für den Bereich erstellt, und die Anzahl der Transaktionen (@@ trancount) erhöht sich nicht. Wenn Sie jedoch TransactionScopeOption.RequiresNew verwenden, wird beim Versuch, die Verbindung in der neuen Transaktion für den Transaktionsbereich zu registrieren, eine hilfreiche Fehlermeldung angezeigt: "Für die Verbindung ist derzeit eine Transaktion registriert. Beenden Sie die aktuelle Transaktion und wiederholen Sie den Vorgang." Und ja, wenn Sie die Transaktion abschließen, in der die Verbindung eingetragen ist, können Sie die Verbindung sicher in eine neue Transaktion eintragen. Update: Wenn Sie zuvor BeginTransaction für die Verbindung aufgerufen haben, wird beim Versuch, eine neue Transaktion für den Transaktionsbereich zu registrieren, ein etwas anderer Fehler ausgegeben: "Die Transaktion kann nicht registriert werden, da für die Verbindung eine lokale Transaktion ausgeführt wird. Beenden Sie die lokale Transaktion und wiederholen." Auf der anderen Seite können Sie BeginTransaction auf der SqlConnection sicher aufrufen, während sie in einer Transaktion für den Transaktionsbereich eingetragen ist. Dadurch wird @@ trancount tatsächlich um eins erhöht, im Gegensatz zur Option Erforderlich für einen verschachtelten Transaktionsbereich, der dies nicht bewirkt erhöhen, ansteigen. Interessanterweise erhalten Sie keine Fehlermeldung, wenn Sie anschließend mit der Option Erforderlich einen weiteren verschachtelten Transaktionsbereich erstellen.

Q9. Ja. Befehle nehmen an jeder Transaktion teil, für die die Verbindung registriert ist, unabhängig davon, welcher aktive Transaktionsbereich im C # -Code enthalten ist.

Triynko
quelle
11
Nachdem ich die Antwort auf Q8 geschrieben habe, stelle ich fest, dass dieses Zeug so kompliziert aussieht wie die Regeln für Magic: The Gathering! Dies ist jedoch schlimmer, da in der TransactionScope-Dokumentation nichts davon erläutert wird.
Triynko
Öffnen Sie für Q3 zwei Verbindungen gleichzeitig mit derselben Verbindungszeichenfolge? Wenn ja, dann ist das eine verteilte Transaktion (auch mit SQL Server 2008)
Randy unterstützt Monica
2
Nein, ich bearbeite den Beitrag zur Klarstellung. Nach meinem Verständnis führt das gleichzeitige Öffnen von zwei Verbindungen unabhängig von der SQL Server-Version immer zu einer verteilten Transaktion. Vor SQL 2008 würde das Öffnen von jeweils nur einer Verbindung mit derselben Verbindungszeichenfolge immer noch einen DT verursachen. Bei SQL 2008 führt das Öffnen einer Verbindung gleichzeitig (wobei niemals zwei gleichzeitig geöffnet sind) mit derselben Verbindungszeichenfolge nicht zu a DT
Triynko
1
Um Ihre Antwort für Q2 zu klären, sollten die beiden Befehle einwandfrei ausgeführt werden, wenn sie nacheinander im selben Thread ausgeführt werden.
Jared Moore
2
Zum Problem der Q3- Heraufstufung
Pseudocoder
19

Gute Arbeit Triynko, Ihre Antworten sehen für mich alle sehr genau und vollständig aus. Einige andere Dinge, auf die ich hinweisen möchte:

(1) Manuelle Eintragung

In Ihrem obigen Code zeigen Sie (korrekt) die manuelle Registrierung wie folgt:

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

Es ist jedoch auch möglich, dies mit Enlist = false in der Verbindungszeichenfolge zu tun.

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

Hier ist noch etwas zu beachten. Wenn conn2 geöffnet wird, weiß der Verbindungspoolcode nicht, dass Sie ihn später in derselben Transaktion wie conn1 eintragen möchten. Dies bedeutet, dass conn2 eine andere interne Verbindung als conn1 erhält. Wenn dann conn2 eingetragen ist, sind jetzt 2 Verbindungen eingetragen, sodass die Transaktion zu MSDTC heraufgestuft werden muss. Diese Aktion kann nur durch die automatische Registrierung vermieden werden.

(2) Vor .Net 4.0 empfehle ich dringend, in der Verbindungszeichenfolge "Transaktionsbindung = Explizite Aufhebung der Bindung" festzulegen . Dieses Problem wurde in .Net 4.0 behoben, sodass Explicit Unbind völlig unnötig wird.

(3) Sich selbst zu rollen CommittableTransactionund sich darauf einzustellen Transaction.Current, ist im Wesentlichen dasselbe wie das, was TransactionScopetut. Dies ist selten wirklich nützlich, nur zu Ihrer Information.

(4) Transaction.Current ist fadenstatisch. Dies bedeutet, dass dies Transaction.Currentnur für den Thread festgelegt ist, der das erstellt hat TransactionScope. Daher sind mehrere Threads, die dasselbe ausführen TransactionScope(möglicherweise mit Task), nicht möglich.

Jared Moore
quelle
Ich habe dieses Szenario gerade getestet und es scheint so zu funktionieren, wie Sie es beschreiben. Selbst wenn Sie die automatische Registrierung verwenden und vor dem Öffnen der zweiten Verbindung "SqlConnection.ClearAllPools ()" aufrufen, wird diese zu einer verteilten Transaktion eskaliert.
Triynko
Wenn dies zutrifft, kann es immer nur eine einzige "echte" Verbindung geben, die an einer Transaktion beteiligt ist. Die Möglichkeit, eine in einer TransactionScope-Transaktion eingetragene Verbindung zu öffnen, zu schließen und erneut zu öffnen, ohne zu einer verteilten Transaktion zu eskalieren, ist dann eine Illusion, die vom Verbindungspool erzeugt wird , der normalerweise die entsorgte Verbindung offen lässt und bei erneuter Verbindung genau dieselbe Verbindung zurückgibt -geöffnet für die automatische Registrierung.
Triynko
Was Sie also wirklich sagen, ist, dass, wenn Sie den automatischen Registrierungsprozess umgehen, Sie eine neue Verbindung innerhalb einer Transaktionsbereichstransaktion (TST) erneut öffnen, anstatt dass der Verbindungspool die richtige Verbindung (die ursprüngliche) abruft im TST eingetragen), greift ganz angemessen eine völlig neue Verbindung auf, die bei manueller Eintragung zu einer Eskalation des TST führt.
Triynko
Genau das habe ich in meiner Antwort auf Q1 angedeutet, als ich erwähnte, dass es eingetragen ist, es sei denn, "Enlist = false" ist in der Verbindungszeichenfolge angegeben, und dann darüber gesprochen, wie der Pool eine geeignete Verbindung findet.
Triynko
Wenn Sie den Link in meiner Antwort auf Q2 besuchen, werden Sie beim Multithreading feststellen, dass Transaction.Current zwar für jeden Thread eindeutig ist, Sie die Referenz jedoch problemlos in einem Thread abrufen und an einen anderen Thread übergeben können. Der Zugriff auf ein TST von zwei verschiedenen Threads führt jedoch zu einem sehr spezifischen Fehler "Transaktionskontext wird von einer anderen Sitzung verwendet". Um einen TST mit mehreren Threads zu erstellen, müssen Sie eine DependantTransaction erstellen. Zu diesem Zeitpunkt muss es sich jedoch um eine verteilte Transaktion handeln, da Sie eine zweite unabhängige Verbindung benötigen, um tatsächlich gleichzeitig Befehle auszuführen, und MSDTC, um die beiden zu koordinieren.
Triynko
1

Eine andere bizarre Situation, die wir gesehen haben, ist, dass wenn Sie eine konstruieren, EntityConnectionStringBuilderdiese mit TransactionScope.Currentder Transaktion in Konflikt gerät und (wir denken) dazu beiträgt. Wir haben dies im Debugger beobachtet, wo TransactionScope.Current‚s current.TransactionInformation.internalTransactionzeigt enlistmentCount == 1vor der Errichtung und enlistmentCount == 2danach.

Um dies zu vermeiden, konstruieren Sie es im Inneren

using (new TransactionScope(TransactionScopeOption.Suppress))

und möglicherweise außerhalb des Bereichs Ihres Betriebs (wir haben es jedes Mal erstellt, wenn wir eine Verbindung benötigten).

Todd
quelle