При каких обстоятельствах 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?)
}
Triynko
источник

Ответы:

188

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

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

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

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

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

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

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

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

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

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

Triynko
источник
11
После написания ответа на вопрос 8 я понимаю, что этот материал начинает выглядеть так же сложно, как и правила Magic: The Gathering! За исключением того, что это хуже, потому что документация TransactionScope не объясняет ничего из этого.
Трийнко
Для Q3, вы открываете два соединения одновременно, используя одну и ту же строку соединения? Если это так, то это будет распределенная транзакция (даже с SQL Server 2008)
Рэнди поддерживает Монику
2
Я редактирую пост, чтобы уточнить. Насколько я понимаю, одновременное открытие двух соединений всегда приведет к распределенной транзакции, независимо от версии SQL Server. До SQL 2008 открытие только одного соединения за раз с одной и той же строкой соединения все равно вызывало бы DT, но в SQL 2008 открытие одного соединения за раз (никогда не имея двух открытых одновременно) с одной и той же строкой соединения не приводило к DT
Triynko
1
Чтобы уточнить ваш ответ на вопрос 2, две команды должны работать нормально, если они выполняются последовательно в одном и том же потоке.
Джаред Мур
2
Что касается вопроса о продвижении Q3 для идентичных строк подключения в SQL 2008, приведем
псевдокодер,
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, что делает явную отмену привязки совершенно ненужной.

(3) Откатить свое CommittableTransactionи установить Transaction.Currentэто по сути то же самое, что и то, что TransactionScopeделает. Это редко на самом деле полезно, просто к вашему сведению.

(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))

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

Тодд
источник