При каких обстоятельствах SqlConnection автоматически зачисляется в внешнюю транзакцию TransactionScope? - PullRequest
194 голосов
/ 21 мая 2010

Что означает, что 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?)
}

Ответы [ 3 ]

179 голосов
/ 22 мая 2010

Я провел несколько тестов с тех пор, как задал этот вопрос, и нашел большинство, если не все ответы самостоятельно, поскольку никто не ответил. Пожалуйста, дайте мне знать, если я что-то пропустил.

Q1. Да, если в строке подключения не указано "enlist = false". Пул соединений находит пригодное для использования соединение. Подходящее соединение - это соединение, которое не зачислено в транзакцию, или соединение, зачисленное в той же транзакции.

Q2. Второе соединение является независимым соединением, которое участвует в той же транзакции. Я не уверен в взаимодействии команд на этих двух соединениях, так как они работают с одной и той же базой данных, но я думаю, что могут возникнуть ошибки, если команды будут выполнены для обеих одновременно: ошибки, подобные "Контекст транзакции используется другим сеансом "

Q3. Да, оно преобразуется в распределенную транзакцию, поэтому включение нескольких соединений, даже с одной и той же строкой соединения, превращает их в распределенную транзакцию, что можно подтвердить, проверив ненулевой GUID в Transaction.Current.TransactionInformation.DistributedIdentifier. * Обновление: я где-то читал, что это исправлено в SQL Server 2008, поэтому MSDTC не используется, когда для обоих подключений используется одна и та же строка подключения (если оба подключения не открыты одновременно). Это позволяет вам открывать соединение и закрывать его несколько раз в рамках транзакции, что может лучше использовать пул соединений, открывая соединения как можно позже и закрывая их как можно скорее.

Q4. Нет. Соединение, открытое при отсутствии активной области транзакции, не будет автоматически зачислено во вновь созданную область транзакции.

Q5. Нет. Если вы не откроете соединение в области транзакции или не подключите существующее соединение в области, в основном, нет транзакции. Чтобы ваши команды могли участвовать в транзакции, ваше соединение должно быть автоматически или вручную зачислено в область транзакции.

Q6. Да, команды на соединении, не участвующем в транзакции, фиксируются как выпущенные, даже если код выполнен в блоке области транзакции, который был откатан. Если соединение не зачислено в текущую область транзакции, оно не участвует в транзакции, поэтому фиксация или откат транзакции не окажут влияния на команды, выполненные для соединения, не включенного в область транзакции ... как this парень узнал . Это очень трудно определить, если вы не понимаете процесс автоматического зачисления: он происходит только тогда, когда соединение открыто внутри активной области транзакции.

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

Q8. Возможно, возникла ошибка.Если использовался TransactionScopeOption.Required и соединение уже было зачислено в транзакцию области транзакции, то ошибки нет;фактически для этой области не было создано новой транзакции, и количество транзакций (@@ trancount) не увеличивается.Однако, если вы используете TransactionScopeOption.RequiresNew, вы получите полезное сообщение об ошибке при попытке включить соединение в новую транзакцию области транзакции: «В данный момент в транзакции подключена транзакция. Завершите текущую транзакцию и повторите попытку».И да, если вы завершите транзакцию, в которую зачислено соединение, вы можете безопасно подключить соединение к новой транзакции. Обновление: если вы ранее вызывали BeginTransaction для соединения, при попытке зачисления в новую транзакцию области транзакции выдается немного другая ошибка: «Невозможно подключиться к транзакции, так как в соединении выполняется локальная транзакция. Завершитьлокальная транзакция и повторите попытку. "С другой стороны, вы можете безопасно вызывать BeginTransaction для SqlConnection, пока он зачислен в транзакцию области транзакции, и это фактически увеличит @@ trancount на единицу, в отличие от использования опции Required области вложенной транзакции, которая не вызывает егоувеличение.Интересно, что если вы затем продолжите создавать другую вложенную область транзакции с параметром Required, вы не получите ошибку, потому что ничего не изменится в результате уже имеющейся транзакции активной области транзакции (помните, @@ trancount не увеличивается, когда транзакциятранзакция области уже активна и используется опция Обязательный).

Q9. Да.Команды участвуют в любой транзакции, в которую включено соединение, независимо от того, какая область активной транзакции находится в коде C #.

19 голосов
/ 28 сентября 2011

Отличная работа, Трайнко, все ваши ответы выглядят довольно точно и полно для меня. Некоторые другие вещи, на которые я хотел бы обратить внимание:

(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, что делает явную отмену привязки совершенно ненужной.

(3) Прокрутка собственного CommittableTransaction и установка Transaction.Current в сущности - это то же самое, что и TransactionScope. На самом деле это редко полезно, просто к вашему сведению.

(4) Transaction.Current является потоково-статическим. Это означает, что Transaction.Current устанавливается только в потоке, который создал TransactionScope. Поэтому несколько потоков, выполняющих один и тот же TransactionScope (возможно, с использованием Task), невозможно.

0 голосов
/ 07 июля 2017

Еще одна странная ситуация, с которой мы столкнулись, заключается в том, что если вы создадите EntityConnectionStringBuilder, он будет гадить с TransactionScope.Current и (мы думаем) подключиться к транзакции. Мы наблюдали это в отладчике, где TransactionScope.Current current.TransactionInformation.internalTransaction показывает enlistmentCount == 1 до построения и enlistmentCount == 2 после.

Чтобы избежать этого, создайте его внутри

using (new TransactionScope(TransactionScopeOption.Suppress))

и, возможно, выходит за рамки вашей операции (мы создавали ее каждый раз, когда нам требовалось соединение).

...