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

person Triynko    schedule 21.05.2010    source источник


Ответы (3)


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

Q1: Автоматически ли включается соединение в транзакцию?

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

Q2: Если я сейчас открою (и запустил команды) второе соединение с идентичной строкой соединения, каковы будут отношения между этим вторым соединением и первым, если таковые имеются?

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

Q3: Будет ли автоматическое включение этого второго соединения в текущую область транзакции привести к эскалации транзакции в распределенную транзакцию?

Да, он перерастает в распределенную транзакцию, поэтому включение более одного соединения, даже с одной и той же строкой соединения, превращает его в распределенную транзакцию, что может быть подтверждено проверкой ненулевого идентификатора GUID в Transaction.Current.TransactionInformation.DistributedIdentifier.

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

Q4: Если я начну выполнять команды в соединении сейчас, будет ли оно автоматически включено в текущую область транзакции?

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

Q5: Если не указан, будут ли команды, которые я выполняю в соединении, теперь участвовать в внешней транзакции?

Нет. Если вы не открываете соединение в области транзакции или не зачисляете существующее соединение в область действия, ТРАНЗАКЦИИ НЕ БУДУТ. Ваше соединение должно быть автоматически или вручную включено в область транзакции, чтобы ваши команды могли участвовать в транзакции.

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

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

В7: Приведенный выше метод явно включает ранее существовавшее соединение в текущую внешнюю транзакцию, так что команды, которые я выполняю в соединении, теперь участвуют в внешней транзакции?

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

В8: что произойдет, если существующее соединение уже было задействовано в транзакции, когда я вызвал вышеуказанный метод? Может возникнуть ошибка?

Может возникнуть ошибка. Если использовалось TransactionScopeOption.Required и соединение уже было включено в транзакцию области транзакции, то ошибки нет; фактически для области не создается новая транзакция, и количество транзакций (@@trancount) не увеличивается. Если, однако, вы используете TransactionScopeOption.RequiresNew, то при попытке включить соединение в новую транзакцию области транзакции вы получите полезное сообщение об ошибке: Соединение в настоящее время содержит транзакцию в списке. Завершите текущую транзакцию и повторите попытку. И да, если вы завершите транзакцию, в которой установлено соединение, вы можете безопасно включить соединение в новую транзакцию.

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

В9: Если существующее соединение уже было зачислено в транзакцию, и я НЕ вызвал вышеуказанный метод для его включения, будут ли какие-либо команды, которые я выполняю на нем, участвовать в его существующей транзакции, а не в текущей области транзакции?

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

person Triynko    schedule 21.05.2010
comment
Написав ответ на вопрос 8, я понимаю, что этот материал начинает выглядеть так же сложно, как правила Magic: The Gathering! Но это еще хуже, потому что документация TransactionScope ничего из этого не объясняет. - person Triynko; 22.05.2010
comment
Для Q3 вы открываете два соединения одновременно, используя одну и ту же строку соединения? Если да, то это будет распределенная транзакция (даже с SQL Server 2008). - person Randy supports Monica; 29.05.2010
comment
Нет. Я редактирую пост, чтобы уточнить. Насколько я понимаю, одновременное открытие двух соединений всегда вызывает распределенную транзакцию, независимо от версии SQL Server. До SQL 2008 открытие только одного соединения за раз с той же строкой соединения по-прежнему приводило к DT, но в SQL 2008 открытие одного соединения за раз (никогда не открывать два одновременно) с одной и той же строкой соединения не приводило к DT - person Triynko; 02.06.2010
comment
Чтобы прояснить ваш ответ для Q2, две команды должны работать нормально, если они выполняются последовательно в одном потоке. - person Jared Moore; 29.09.2011
comment
Что касается проблемы продвижения в третьем квартале для идентичных строк подключения в SQL 2008, вот цитата из MSDN: msdn.microsoft.com/en-us/library/ms172070 (v = vs.90) .aspx - person pseudocoder; 26.07.2012
comment
@Triynko Пожалуйста, повторите вопросы перед ответами. - person Bruno Martinez; 06.10.2013
comment
stackoverflow.com/questions/39410682/ Посмотрите мою проблему, есть ли решение? - person Ranvijay Singh; 12.09.2016
comment
Очень обстоятельный ответ !!! Для полноты, если вы хотите полностью обойти транзакцию и вложить запрос dapper, заключите код dapper в новый TransactionScope следующим образом: using (var scope = new TransactionScope (TransactionScopeOption.Suppress)) - person PanKak; 30.06.2017
comment
+1 за то, как организован вопрос, и +1 за отличную работу по поиску ответов. Кроме того, у меня есть еще один вопрос в сценарии 2. После выполнения: conn.EnlistTransaction( Transaction.Current ); Могу ли я с уверенностью предположить, что любые команды, выполненные до этой строки, будут уже зафиксированы, и новая включенная транзакция не влияет на старые команды? (Я думаю да). - person Nour; 30.09.2018

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

(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 я настоятельно рекомендую установить "Transaction Binding = Explicit Unbind" в строке подключения. Эта проблема исправлена ​​в .Net 4.0, что сделало явную отмену привязки совершенно ненужной.

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

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

person Jared Moore    schedule 28.09.2011
comment
Я только что протестировал этот сценарий, и, похоже, он работает так, как вы описываете. Кроме того, даже если вы используете автоматическое зачисление, если вы вызываете SqlConnection.ClearAllPools () перед открытием второго соединения, оно перерастает в распределенную транзакцию. - person Triynko; 28.09.2011
comment
Если это так, то в транзакции может быть только одно реальное соединение. Возможность открывать, закрывать и повторно открывать соединение, указанное в транзакции TransactionScope, без эскалации до распределенной транзакции, тогда на самом деле является иллюзией, создаваемой пулом соединений, который обычно оставляет удаленное соединение открытым, и вернуть то же самое точное соединение, если оно будет повторно открыто для автоматического включения. - person Triynko; 28.09.2011
comment
Итак, на самом деле вы говорите, что если вы обойдете процесс автоматического зачисления, тогда, когда вы перейдете, чтобы повторно открыть новое соединение внутри транзакции области транзакции (TST), вместо того, чтобы пул соединений захватил правильное соединение (первоначально включенный в TST), он вполне уместно захватывает совершенно новое соединение, которое при включении вручную вызывает эскалацию TST. - person Triynko; 28.09.2011
comment
В любом случае, это именно то, на что я намекал в своем ответе на Q1, когда упомянул, что он включен в список, если в строке подключения не указано Enlist = false, а затем рассказал о том, как пул находит подходящее соединение. - person Triynko; 29.09.2011
comment
Что касается многопоточности, если вы перейдете по ссылке в моем ответе на Q2, вы увидите, что хотя Transaction.Current уникален для каждого потока, вы можете легко получить ссылку в одном потоке и передать ее другому потоку; однако доступ к TST из двух разных потоков приводит к очень специфической ошибке. Контекст транзакции используется другим сеансом. Для многопоточности TST вы должны создать DependantTransaction, но в этот момент это должна быть распределенная транзакция, потому что вам нужно второе независимое соединение для фактического выполнения одновременных команд и MSDTC для координации этих двух. - person Triynko; 29.09.2011
comment
Да, вы совершенно правы. Чтобы уточнить, когда открывается второе соединение, пул будет пытаться использовать существующее объединенное соединение, которое было открыто, а затем закрыто вне транзакции. Другими словами, если вы откроете 2 соединения, а затем закроете их перед созданием TransactionScope, то приведенный выше код будет использовать всего 2 соединения. - person Jared Moore; 29.09.2011
comment
Спасибо за информацию о DependentTransaction, она очень полезна. Многопоточность все еще может быть проблемой, если у вас только одно соединение, например если вы используете Task, await и т. д. - person Jared Moore; 29.09.2011
comment
Я только что перечитал ваш основной пост и увидел, что вы уже прекрасно описали поведение пула в первом квартале. :) - person Jared Moore; 29.09.2011
comment
Правда, если вы хотите избежать распределенной транзакции, насколько я могу судить, многопоточность не подходит. После того, как соединение зачислено в транзакцию, любое последующее повторное открытие соединения должно быть выполнено с помощью автоматического включения, открыв его с Enlist = true (по умолчанию) в блоке области транзакции в том же потоке, поэтому пул возвращает то же самое. точное соединение. Любой другой метод приведет либо к ошибке, либо к добавлению нового подключения к транзакции и, таким образом, к переходу в распределенную транзакцию. - person Triynko; 29.09.2011
comment
@JaredMoore Мне интересно, что две версии, которые вы предоставили (включение и выключение), имеют точно такое же поведение? - person peter; 17.11.2017
comment
Нет, это не так. В первом примере используется только 1 базовое соединение, во втором - 2 базовых подключения, поэтому он становится распределенным (что обычно плохо, поскольку оно медленнее и сложнее). Избежать распределенных транзакций таким образом - цель Enlist = true. - person Jared Moore; 05.06.2018

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

Чтобы этого не произошло, сконструируйте его внутри

using (new TransactionScope(TransactionScopeOption.Suppress))

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

person Todd    schedule 06.07.2017