За яких обставин SqlConnection автоматично зараховується до зовнішньої транзакції TransactionScope?


201

Що означає "включення" SqlConnection в транзакцію? Чи просто це означає, що в транзакції будуть брати участь команди, які я виконую під час з'єднання?

Якщо так, то за яких обставин SqlConnection автоматично зараховується до зовнішньої транзакції TransactionScope?

Дивіться питання в коментарях до коду. Моя здогадка на відповідь на кожне питання слідує за кожним питанням у дужках.

Сценарій 1: Відкриття з'єднань Внутрішній обсяг транзакції

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

Сценарій 2: Використання з'єднань Внутрішньо обсяг транзакції, які були відкриті ЗОВН

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

Відповіді:


188

Я зробив декілька тестів з часу, коли я поставив це запитання, і знайшов більшість, якщо не всі відповіді самостійно, оскільки ніхто більше не відповів. Будь ласка, дайте мені знати, якщо я щось пропустив.

Q1. Так, якщо в рядку з'єднання не вказано "enlist = false". Пул з'єднання знаходить корисне з'єднання. Корисне з'єднання - це той, який не занесений до трансакції, або той, що зарахований до тієї самої транзакції.

Q2. Другий зв'язок - це незалежне з'єднання, яке бере участь в одній і тій же транзакції. Я не впевнений у взаємодії команд на цих двох з'єднаннях, оскільки вони працюють проти однієї бази даних, але я думаю, що помилки можуть виникати, якщо команди видаються на обох одночасно: помилки типу "Контекст транзакцій у використанні ще один сеанс "

Q3. Так, він переходить до розподіленої транзакції, тому включення більше ніж одне з'єднання, навіть із однією і тією ж ланцюжкою з'єднання, призводить до того, що воно стає розподіленою транзакцією, що може бути підтверджено шляхом перевірки наявності ненульового GUID на Transaction.Current.TransactionInformation .Розподілений ідентифікатор. * Оновлення: я десь прочитав, що це виправлено в SQL Server 2008, так що MSDTC не використовується, коли однакова рядок з'єднання використовується для обох з'єднань (до тих пір, поки обидва з'єднання не відкриваються одночасно). Це дозволяє відкрити з'єднання та закрити його кілька разів у межах транзакції, що могло б краще використовувати пул зв’язків, відкриваючи з'єднання якомога пізніше та закриваючи їх якнайшвидше.

Q4. Ні. З'єднання, відкрите, коли жодна область транзакцій не була активною, не буде автоматично зарахована до новоствореної області транзакцій.

Q5. Ні. Якщо ви не відкриєте з'єднання в межах транзакції або не зареєструєте наявне з'єднання в області дії, в основному НЕ ПЕРЕКЛАДА. Для того, щоб ваші команди брали участь у транзакції, ваше з'єднання повинно бути автоматично або вручну занесено до сфери транзакції.

Q6. Так, команди на з'єднанні, що не бере участь у транзакції, виконуються як видані, навіть незважаючи на те, що код виконується в блоці області транзакції, який повернувся назад. Якщо з'єднання не занесено до поточної сфери транзакції, воно не бере участі в транзакції, тому здійснення або скасування транзакції не матиме впливу на команди, видані на з'єднання, не включене в область транзакції ... як це дізнався хлопець . Це дуже важко помітити, якщо ви не розумієте процес автоматичного зарахування: він відбувається лише тоді, коли з'єднання відкривається всередині активного діапазону транзакцій.

Q7. Так. Існуюче з'єднання можна явно занести до поточної області транзакцій, викликавши EnlistTransaction (Transaction.Current). Ви також можете зарахувати з'єднання в окремий потік транзакції, використовуючи DependentTransaction, але, як і раніше, я не впевнений, як можуть взаємодіяти два з'єднання, що беруть участь в одній транзакції проти однієї бази даних ... і можуть виникати помилки, і звичайно, друге зараховане з'єднання змушує транзакцію переходити до розподіленої транзакції.

Q8. Можлива помилка. Якщо TransactionScopeOption.Required було використано, а з'єднання вже було зараховано до транзакції за обсягом транзакції, помилки немає; насправді не створено нової транзакції для сфери застосування, і кількість транзакцій (@@ trancount) не збільшується. Якщо ви користуєтеся TransactionScopeOption.RequiresNew, тоді ви отримуєте корисне повідомлення про помилку при спробі зарахувати з'єднання в новій транзакції обсягу транзакції: "Підключення наразі зареєстровано транзакцією. Закінчіть поточну транзакцію та повторіть спробу." І так, якщо ви завершите транзакцію, до якої пов’язано з'єднання, ви можете сміливо зараховувати підключення до нової транзакції. Оновлення: Якщо раніше ви покликали BeginTransaction під час з'єднання, при спробі зарахування на нову транзакцію щодо обсягу трансакції виникла дещо інша помилка: "Неможливо зарахувати транзакцію, оскільки локальна транзакція проводиться підключенням. Закінчіть локальну транзакцію та повторіть спробу ". З іншого боку, ви можете сміливо викликати BeginTransaction на SqlConnection, в той час як його зараховано до транзакції обсягу транзакції, і це насправді збільшить транзакцію @@ на одну, на відміну від використання необхідної опції вкладеного обсягу транзакції, що не спричиняє її збільшити. Цікаво, що якщо ви продовжите створити інший вкладений обсяг транзакції з опцією «Обов’язковий», ви не отримаєте помилку,

Q9. Так. Команди беруть участь у будь-якій транзакції, до якої пов’язано з'єднання, незалежно від того, який активний обсяг транзакції знаходиться в коді C #.


11
Написавши відповідь на Q8, я розумію, що цей матеріал починає виглядати так само складно, як правила для Magic: The Gathering! За винятком цього гірше, тому що документація TransactionScope нічого з цього не пояснює.
Трайко

Для Q3 ви відкриваєте два з'єднання одночасно, використовуючи одну і ту ж рядок з'єднання? Якщо так, то це буде розподілена трансакція (навіть із SQL Server 2008)
Ренді підтримує Моніку

2
Ні. Я редагував пост, щоб уточнити. Я розумію, що відкриття двох з'єднань одночасно завжди призведе до розподіленої транзакції, незалежно від версії SQL Server. До SQL 2008 відкриття лише одного з'єднання одночасно, з тим самим рядком з'єднання все ще спричиняло б DT, але для SQL 2008 відкриття одного з'єднання одночасно (ніколи не мати двох відкритих одночасно) з тим же рядком з'єднання не спричинить DT
Трайнко

1
Щоб уточнити свою відповідь на Q2, обидві команди повинні працювати добре, якщо вони будуть виконані послідовно на одній нитці.
Джаред Мур

2
Щодо питання просування Q3 для однакових рядків підключення в SQL 2008, ось цитування MSDN: msdn.microsoft.com/en-us/library/ms172070(v=vs.90).aspx
псевдокодер

19

Приємна робота Трайко, всі ваші відповіді виглядають для мене досить точними та повноцінними. Ще деякі речі, які я хотів би зазначити:

(1) Зарахування вручну

У своєму коді вище, ви (правильно) показуєте ручне зарахування так:

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

Однак це також можна зробити так, використовуючи Enlist = false у рядку з'єднання.

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

Тут слід зазначити ще одну річ. Коли conn2 відкрито, код пулу з'єднання не знає, що ви хочете пізніше зарахувати його до тієї самої транзакції, що і conn1, що означає, що conn2 має внутрішнє з'єднання, ніж conn1. Тоді, коли conn2 зараховано до списку, тепер є 2 з'єднання, занесені до транзакції, тому транзакцію потрібно просувати до MSDTC. Цю акцію можна уникнути лише за допомогою автоматичного зарахування.

(2) Перед .Net 4.0 я настійно рекомендую встановити "Прив'язка транзакцій = Явне скасування" в рядку з'єднання . Ця проблема виправлена ​​в .Net 4.0, що робить Explicit Unbind абсолютно непотрібним.

(3) Згорнути свою власну CommittableTransactionі встановити Transaction.Currentце - це те саме, що і те, що TransactionScopeробить. Це рідко є фактично корисним, лише FYI.

(4) Transaction.Current є нитковим статичним. Це означає, що Transaction.Currentвстановлено лише на потоці, який створив TransactionScope. Тому кілька потоків, які виконують одне і те ж TransactionScope(можливо, використання Task), неможливо.


Я просто перевірив цей сценарій, і він, здається, працює так, як ви описуєте. Крім того, навіть якщо ви використовуєте автоматичне зарахування, якщо ви відкриваєте "SqlConnection.ClearAllPools ()" перед відкриттям другого з'єднання, воно переходить до розподіленої транзакції.
Трайнко

Якщо це правда, тоді в транзакції може бути лише одне "реальне" з'єднання. Можливість відкрити, закрити та повторно відкрити з'єднання, що зараховане до транзакції TransactionScope, не переходячи до розподіленої транзакції, тоді справді є ілюзією, створеною пулом з'єднань , яка, як правило, залишає розміщене з'єднання відкритим, і повертає те саме точне з'єднання, якщо повторно -відкрито для автоматичного зарахування.
Трайнко

Тож, що ви насправді говорите, це те, що якщо ви переходите до процесу автоматичного зарахування, тоді, коли ви перейдете на повторне відкриття нового підключення всередині транзакції за обсягом транзакції (TST), замість пулу з'єднань, що захоплює правильне з'єднання (вихідне зарахований до TST), він цілком доречно захоплює абсолютно нове з'єднання, яке при зарахуванні вручну призводить до ескалації TST.
Трайнко

У всякому разі, саме на це я натякав у своїй відповіді на Q1, коли я згадав, що це зараховано, якщо в рядку з'єднання не вказано "Enlist = false", а потім розповів про те, як пул знаходить підходяще з'єднання.
Трайнко

Що стосується багатопотокової передачі, якщо ви відвідаєте посилання у моїй відповіді на Q2, ви побачите, що, хоча Transaction.Current є унікальним для кожного потоку, ви можете легко отримати посилання в одному потоці та передати його в інший потік; однак, доступ до TST з двох різних потоків призводить до дуже специфічної помилки "Контекст транзакції, який використовується іншим сеансом". Для багатопотокового TST потрібно створити DependantTransaction, але в цей момент він повинен бути розподіленою транзакцією, оскільки вам потрібно друге незалежне з'єднання для фактичного виконання одночасних команд і MSDTC для координації двох.
Трайнко

1

Ще одна химерна ситуація, яку ми бачили, - це те, що якщо ви сконструюєте, EntityConnectionStringBuilderвін зіткнеться з TransactionScope.Currentта (ми думаємо) зарахуйте до транзакції. Ми спостерігали це в відладчик, де TransactionScope.Current«S current.TransactionInformation.internalTransactionпоказує , enlistmentCount == 1перш ніж будувати, а enlistmentCount == 2потім.

Щоб цього уникнути, побудуйте його всередині

using (new TransactionScope(TransactionScopeOption.Suppress))

і, можливо, поза сферою вашої операції (ми будували її кожен раз, коли нам було потрібно з'єднання).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.