W jakich okolicznościach SqlConnection jest automatycznie rejestrowany w otaczającej TransactionScope Transaction?

201

Co to znaczy, że SqlConnection może zostać „zarejestrowany” w transakcji? Czy to po prostu oznacza, że ​​polecenia, które wykonam na połączeniu, będą uczestniczyć w transakcji?

Jeśli tak, to w jakich okolicznościach SqlConnection jest automatycznie rejestrowany w otaczającej TransactionScope Transaction?

Zobacz pytania w komentarzach do kodu. Domyślam się, że odpowiedź na każde pytanie wynika z każdego pytania w nawiasie.

Scenariusz 1: Otwieranie połączeń WEWNĄTRZ zakresu transakcji

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

Scenariusz 2: Korzystanie z połączeń WEWNĄTRZ zakresu transakcji, który został otwarty POZA nim

//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
źródło

Odpowiedzi:

188

Po zadaniu tego pytania przeprowadziłem kilka testów i znalazłem większość, jeśli nie wszystkie odpowiedzi, na własną rękę, ponieważ nikt inny nie odpowiedział. Daj mi znać, jeśli coś przeoczyłem.

Pytanie 1 Tak, chyba że w parametrze połączenia podano „enlist = false”. Pula połączeń znajduje użyteczne połączenie. Użyteczne połączenie to takie, które nie jest zarejestrowane w transakcji lub takie, które jest zarejestrowane w tej samej transakcji.

Q2 Drugie połączenie jest niezależnym połączeniem, które uczestniczy w tej samej transakcji. Nie jestem pewien interakcji między tymi dwoma połączeniami, ponieważ działają one na tej samej bazie danych, ale myślę, że mogą wystąpić błędy, jeśli polecenia zostaną wydane dla obu jednocześnie: błędy typu „Kontekst transakcji używany przez kolejna sesja ”

Pytanie 3 Tak, zostaje eskalowany do transakcji rozproszonej, więc rejestracja więcej niż jednego połączenia, nawet z tym samym łańcuchem połączenia, powoduje, że staje się transakcją rozproszoną, co można potwierdzić, sprawdzając identyfikator GUID o wartości innej niż null w Transaction.Current.TransactionInformation .DistributIdentifier. * Aktualizacja: Przeczytałem gdzieś, że jest to naprawione w SQL Server 2008, więc MSDTC nie jest używany, gdy ten sam ciąg połączenia jest używany dla obu połączeń (o ile oba połączenia nie są otwarte w tym samym czasie). Pozwala to na otwarcie połączenia i zamknięcie go wiele razy w ramach transakcji, co może lepiej wykorzystać pulę połączeń, otwierając połączenia tak późno, jak to możliwe i zamykając je jak najszybciej.

Pytanie 4 Nie. Połączenie otwarte, gdy żaden zakres transakcji nie był aktywny, nie zostanie automatycznie zarejestrowane w nowo utworzonym zakresie transakcji.

Pytanie 5 Nie. O ile nie otworzysz połączenia w zakresie transakcji lub nie zarejestrujesz istniejącego połączenia w tym zakresie, zasadniczo NIE MA TRANSAKCJI. Twoje połączenie musi zostać automatycznie lub ręcznie zarejestrowane w zakresie transakcji, aby polecenia mogły uczestniczyć w transakcji.

Pytanie 6 Tak, polecenia dotyczące połączenia nieuczestniczącego w transakcji są zatwierdzane w postaci wydanej, nawet jeśli kod wykonał się w bloku zakresu transakcji, który został wycofany. Jeśli połączenie nie jest zarejestrowane w bieżącym zakresie transakcji, nie bierze udziału w transakcji, więc zatwierdzenie lub wycofanie transakcji nie będzie miało wpływu na polecenia wydane dla połączenia niezarejestrowanego w zakresie transakcji ... jak się dowiedział ten facet . Jest to bardzo trudne do wykrycia, chyba że rozumiesz automatyczny proces rejestracji: występuje tylko wtedy, gdy połączenie jest otwarte w aktywnym zakresie transakcji.

Pytanie 7 Tak. Istniejące połączenie można jawnie zarejestrować w bieżącym zakresie transakcji, wywołując EnlistTransaction (Transaction.Current). Możesz również zarejestrować połączenie w oddzielnym wątku w transakcji za pomocą DependentTransaction, ale podobnie jak wcześniej, nie jestem pewien, w jaki sposób dwa połączenia zaangażowane w tę samą transakcję z tą samą bazą danych mogą oddziaływać ... i mogą wystąpić błędy, i oczywiście drugie zarejestrowane połączenie powoduje eskalację transakcji do transakcji rozproszonej.

Pytanie 8 Błąd może zostać zgłoszony. Jeśli użyto TransactionScopeOption.Required, a połączenie było już zarejestrowane w transakcji o zasięgu transakcji, nie wystąpił błąd; w rzeczywistości nie utworzono nowej transakcji dla zakresu, a liczba transakcji (@@ trancount) nie wzrasta. Jeśli jednak użyjesz TransactionScopeOption.RequiresNew, otrzymasz pomocny komunikat o błędzie przy próbie zarejestrowania połączenia w nowej transakcji zakresu transakcji: „Połączenie aktualnie ma zarejestrowaną transakcję. Zakończ bieżącą transakcję i spróbuj ponownie”. I tak, jeśli zakończysz transakcję, w której zarejestrowano połączenie, możesz bezpiecznie zarejestrować połączenie w nowej transakcji. Aktualizacja: jeśli wcześniej wywołałeś BeginTransaction dla połączenia, przy próbie zarejestrowania w nowej transakcji zakresu transakcji generowany jest nieco inny błąd: „Nie można zarejestrować w transakcji, ponieważ trwa połączenie transakcji lokalnej. Zakończ transakcję lokalną i spróbować ponownie." Z drugiej strony możesz bezpiecznie wywołać BeginTransaction na SqlConnection, gdy jest on zarejestrowany w transakcji zakresu transakcji, a to faktycznie zwiększy @@ trancount o jeden, w przeciwieństwie do korzystania z opcji Wymagany zagnieżdżonego zakresu transakcji, co nie powoduje, że zwiększać. Co ciekawe, jeśli następnie utworzysz kolejny zakres transakcji zagnieżdżonych z opcją Wymagane, nie pojawi się błąd,

Pytanie 9 Tak. Polecenia uczestniczą w każdej transakcji, w której zarejestrowane jest połączenie, niezależnie od tego, w jakim aktywnym zakresie transakcji jest kod C #.

Triynko
źródło
11
Po napisaniu odpowiedzi na pytanie 8, zdaję sobie sprawę, że te rzeczy zaczynają wyglądać tak skomplikowane, jak zasady gry Magic: The Gathering! Z wyjątkiem tego, że jest gorzej, ponieważ dokumentacja TransactionScope nie wyjaśnia tego.
Triynko
Czy w trzecim kwartale otwierasz dwa połączenia w tym samym czasie, używając tego samego ciągu połączenia? Jeśli tak, to będzie to transakcja rozproszona (nawet z SQL Server 2008)
Randy obsługuje Monikę
2
Nie. Edytuję post, aby wyjaśnić. Rozumiem, że posiadanie dwóch połączeń jednocześnie spowoduje zawsze rozproszoną transakcję, niezależnie od wersji SQL Server. Przed SQL 2008 otwarcie tylko jednego połączenia na raz z tym samym ciągiem połączenia wciąż powodowałoby ID, ale w przypadku SQL 2008 otwarcie jednego połączenia na raz (nigdy nie otwieranie dwóch jednocześnie z tym samym ciągiem połączenia nie spowoduje DT
Triynko
1
Aby wyjaśnić odpowiedź na pytanie 2, oba polecenia powinny działać poprawnie, jeśli są wykonywane sekwencyjnie w tym samym wątku.
Jared Moore,
2
W kwestii promocji Q3 dla identycznych parametrów połączenia w SQL 2008, oto cytat MSDN: msdn.microsoft.com/en-us/library/ms172070(v=vs.90).aspx
pseudokoder
19

Dobra robota, Triynko, wszystkie twoje odpowiedzi wydają mi się dość dokładne i kompletne. Kilka innych rzeczy, na które chciałbym zwrócić uwagę:

(1) Rejestracja ręczna

W powyższym kodzie pokazujesz (poprawnie) ręczne rejestrowanie w następujący sposób:

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

Jednak można to również zrobić w ten sposób, używając Enlist = false w ciągu połączenia.

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

Należy zwrócić uwagę na jeszcze jedną rzecz. Po otwarciu conn2 kod puli połączeń nie wie, że chcesz go później zarejestrować w tej samej transakcji, co conn1, co oznacza, że ​​conn2 otrzymuje inne połączenie wewnętrzne niż conn1. Następnie, gdy rejestrowane jest conn2, rejestrowane są teraz 2 połączenia, więc transakcja musi być promowana do MSDTC. Tej promocji można uniknąć tylko przy użyciu automatycznej rejestracji.

(2) Przed .Net 4.0 bardzo polecam ustawienie „Transaction Binding = Explicit Unbind” w ciągu połączenia . Ten problem został rozwiązany w .Net 4.0, dzięki czemu Explicit Unbind jest całkowicie niepotrzebny.

(3) Przetaczanie własnych CommittableTransactioni ustawianie Transaction.Currentdo tego jest zasadniczo tym samym, co TransactionScopeto, co robi. Jest to rzadko przydatne, po prostu FYI.

(4) Transaction.Current jest statyczny. Oznacza to, że Transaction.Currentjest ustawiony tylko w wątku, który utworzył TransactionScope. Tak więc wiele wątków wykonujących to samo TransactionScope(prawdopodobnie wykorzystujących Task) nie jest możliwe.

Jared Moore
źródło
Właśnie przetestowałem ten scenariusz i wydaje się, że działa on tak, jak opisano. Ponadto, nawet jeśli korzystasz z automatycznego rejestrowania, jeśli wywołasz „SqlConnection.ClearAllPools ()” przed otwarciem drugiego połączenia, zostanie ono eskalowane do transakcji rozproszonej.
Triynko
Jeśli jest to prawda, wówczas transakcja może dotyczyć tylko jednego „rzeczywistego” połączenia. Zdolność do otwierania, zamykania i ponownego otwierania połączenia zarejestrowanego w transakcji TransactionScope bez eskalacji do transakcji rozproszonej jest wtedy złudzeniem stworzonym przez pulę połączeń , która normalnie pozostawiłaby zbywane połączenie otwarte i zwróciłaby to samo dokładne połączenie, jeśli ponownie -otwarty do automatycznej rejestracji.
Triynko
Więc tak naprawdę mówisz, że jeśli pomijasz automatyczny proces rejestracji, to kiedy idziesz, aby ponownie otworzyć nowe połączenie w ramach transakcji zakresu transakcji (TST), zamiast puli połączeń przechwytującej prawidłowe połączenie (pierwotnie to zarejestrowany w TST), całkiem odpowiednio pobiera zupełnie nowe połączenie, które po ręcznym zarejestrowaniu powoduje eskalację TST.
Triynko
W każdym razie właśnie o tym wspominałem w mojej odpowiedzi na pytanie 1, kiedy wspomniałem, że jest on zapisany, chyba że w parametrze połączenia podano „Enlist = false”, a następnie mówił o tym, jak pula znajdzie odpowiednie połączenie.
Triynko
Jeśli chodzi o wielowątkowość, jeśli odwiedzisz link w mojej odpowiedzi na Q2, zobaczysz, że chociaż Transaction.Current jest unikalny dla każdego wątku, możesz łatwo uzyskać referencję w jednym wątku i przekazać go do innego wątku; jednak dostęp do TST z ​​dwóch różnych wątków powoduje bardzo specyficzny błąd „Kontekst transakcji używany przez inną sesję”. Aby wielowątkowy TST, musisz utworzyć DependantTransaction, ale w tym momencie musi to być transakcja rozproszona, ponieważ potrzebujesz drugiego niezależnego połączenia, aby faktycznie uruchomić jednoczesne polecenia i MSDTC w celu skoordynowania obu.
Triynko
1

Inną dziwaczną sytuacją, którą widzieliśmy, jest to, że jeśli EntityConnectionStringBuilderją zbudujesz, to się zepsuje TransactionScope.Currenti (naszym zdaniem) zarejestruje się w transakcji. Zauważyliśmy to w debugger, gdzie TransactionScope.Current„s current.TransactionInformation.internalTransactionpokazy enlistmentCount == 1przed konstruowania, a enlistmentCount == 2potem.

Aby tego uniknąć, zbuduj go w środku

using (new TransactionScope(TransactionScopeOption.Suppress))

i być może poza zakresem twojej operacji (budowaliśmy to za każdym razem, gdy potrzebowaliśmy połączenia).

Todd
źródło