При каких обстоятельствах SqlConnection автоматически зачисляется в внешнюю транзакцию TransactionScope?
Что означает, что 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 ответа
С тех пор, как я задал этот вопрос, я провел несколько тестов и сам нашел большинство, если не все ответы, так как никто не ответил. Пожалуйста, дайте мне знать, если я что-то пропустил.
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, вы не получите ошибку, потому что ничего не изменится в результате уже имеющейся активной транзакции области транзакции (помните, @@ trancount не увеличивается, когда транзакция транзакция области уже активна и используется опция Required).
Q9. Да. Команды участвуют в любой транзакции, в которую подключено соединение, независимо от того, какая область активной транзакции находится в коде C#.
Отличная работа, Трайнко, все ваши ответы выглядят довольно точно и полно для меня. Некоторые другие вещи, на которые я хотел бы обратить внимание:
(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
делает. На самом деле это редко полезно, просто к вашему сведению.
(4) Transaction.Current
является потоковым Это означает, что Transaction.Current
устанавливается только в потоке, который создал TransactionScope
, Таким образом, несколько потоков выполняются одинаково TransactionScope
(возможно, используя Task
) это невозможно.
Еще одна странная ситуация, которую мы видели, заключается в том, что если EntityConnectionStringBuilder
это будет гадить с TransactionScope.Current
и (мы думаем) заручиться поддержкой в сделке. Мы наблюдали это в отладчике, где TransactionScope.Current
"s current.TransactionInformation.internalTransaction
шоу enlistmentCount == 1
перед построением, и enlistmentCount == 2
после этого.
Чтобы избежать этого, постройте его внутри
using (new TransactionScope(TransactionScopeOption.Suppress))
и, возможно, выходит за рамки вашей операции (мы создавали ее каждый раз, когда нам требовалось подключение).