При каких обстоятельствах 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 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 existing transaction rather than
    // the current transaction scope. (Yes?)
}

Ответ 1

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

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 области вложенных транзакций, что не приводит к ее увеличение. Интересно, что если вы затем создадите еще одну вложенную область транзакций с параметром "Обязательный", вы не получите ошибку, потому что ничего не изменилось из-за того, что уже имеет транзакцию с активной транзакцией (помните, @@trancount не увеличивается, когда транзакция транзакция области видимости уже активна и используется параметр Обязательный).

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

Ответ 2

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

(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, что делает явным Unbind совершенно ненужным.

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

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

Ответ 3

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

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

using (new TransactionScope(TransactionScopeOption.Suppress))

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