Пул соединений поврежден вложенными транзакциями ADO.NET (с MSDTC)

Нигде не могу найти ответа.

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

Чтобы столкнуться с проблемой, нам потребуется:

  1. быть в распределенной транзакции
  2. вложенное sqlconnection и его sqltransaction в другие sqlconnection и sqltransaction
  3. выполнить откат (явный или неявный - просто не фиксировать) вложенный sqltransaction

Когда пул соединений поврежден, каждый sqlConnection.Open () выдает одно из:

  • SqlException: новый запрос не может быть запущен, потому что он должен иметь действительный дескриптор транзакции.
  • SqlException: распределенная транзакция завершена. Либо зарегистрируйте этот сеанс в новой транзакции, либо в транзакции NULL.

Внутри ADO.NET существует своего рода гонка потоков. Если я поставлю Thread.Sleep(10) где-нибудь в коде, полученное исключение может измениться на второе. Иногда меняется без каких-либо модификаций.


Как воспроизвести

  1. Включите службу Windows Координатора распределенных транзакций (по умолчанию она включена).
  2. Создайте пустое консольное приложение.
  3. Создайте 2 базы данных (может быть пустым) или 1 базу данных и раскомментируйте строку: Transaction.Current.EnlistDurable[...]
  4. Скопируйте и вставьте следующий код:

var connectionStringA = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
            @".\YourServer", "DataBaseA");
var connectionStringB = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
            @".\YourServer", "DataBaseB");

try
{
    using (var transactionScope = new TransactionScope())
    {
        //we need to force promotion to distributed transaction:
        using (var sqlConnection = new SqlConnection(connectionStringA))
        {
            sqlConnection.Open();
        }
        // you can replace last 3 lines with: (the result will be the same)
        // Transaction.Current.EnlistDurable(Guid.NewGuid(), new EmptyIEnlistmentNotificationImplementation(), EnlistmentOptions.EnlistDuringPrepareRequired);

        bool errorOccured;
        using (var sqlConnection2 = new SqlConnection(connectionStringB))
        {
            sqlConnection2.Open();
            using (var sqlTransaction2 = sqlConnection2.BeginTransaction())
            {
                using (var sqlConnection3 = new SqlConnection(connectionStringB))
                {
                    sqlConnection3.Open();
                    using (var sqlTransaction3 = sqlConnection3.BeginTransaction())
                    {
                        errorOccured = true;
                        sqlTransaction3.Rollback();
                    }
                }
                if (!errorOccured)
                {
                    sqlTransaction2.Commit();
                }
                else
                {
                    //do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2
                }
            }
        }
        if (!errorOccured)
            transactionScope.Complete();
    }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Then:

for (var i = 0; i < 10; i++) //all tries will fail
{
    try
    {
        using (var sqlConnection1 = new SqlConnection(connectionStringB))
        {
            // Following line will throw: 
            // 1. SqlException: New request is not allowed to start because it should come with valid transaction descriptor.
            // or
            // 2. SqlException: Distributed transaction completed. Either enlist this session in a new transaction or the NULL transaction.
            sqlConnection1.Open();
            Console.WriteLine("Connection successfully open.");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}


Известные плохие решения и что интересного можно наблюдать

Неудачные решения:

  1. Внутри вложенной sqltransaction с использованием блока do:
    sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);

  2. Замените все SqlTransactions на TransactionScopes (TransactionScope должен обернуть SqlConnection.Open())

  3. Во вложенном блоке используйте sqlconnection из внешнего блока

Интересные наблюдения:

  1. Если приложение подождет пару минут после закрытия пула соединений, все будет работать нормально. Таким образом, защита пула соединений длится всего пару минут.

  2. С прикрепленным отладчиком. Когда выполнение покидает внешнюю sqltransaction, генерируется блок SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.. try ... catch .... не может уловить это исключение.


Как это решить?

Эта проблема делает мое веб-приложение почти мертвым (не может открыть какое-либо новое соединение sql).
Представленный фрагмент кода извлекается из всего конвейера, который также включает вызовы сторонних фреймворков. Я не могу просто изменить код.

  • Кто-нибудь знает, что именно идет не так?
  • Это ошибка ADO.NET?
  • Может, я (и некоторые фреймворки ...) что-то не так делаю?


Мое окружение (кажется, это не очень важно)

  • .NET Framework 4.5
  • MS SQL Server 2012

person owerkop    schedule 20.05.2014    source источник
comment
Я бы сказал, что ваше плохое решение №2 - правильный способ сделать это. Я не уверен, почему вы предпочитаете SqlTransaction TransactionScope.   -  person Josh    schedule 12.02.2015


Ответы (1)


Я знаю, что этот вопрос задавали давно, но я думаю, что у меня есть ответ для всех, у кого эта проблема все еще есть.

Вложенные транзакции в SQL не такие, какими они были бы в структуре кода, который их создает.

Независимо от того, сколько существует вложенных транзакций, имеет значение только внешняя транзакция.

Чтобы внешняя транзакция могла зафиксироваться, внутренние транзакции должны фиксироваться, другими словами, внутренние транзакции не действуют, если они фиксируются - внешняя транзакция все равно должна фиксироваться для завершения транзакции.

Однако, если внутренняя транзакция откатывается, внешняя транзакция откатывается до своего начала. Внешняя транзакция все еще должна откатиться или зафиксироваться - или она все еще в своем начальном состоянии.

Следовательно, в приведенном выше примере строка

//do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2

должно быть

sqlTransaction2.Rollback();

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

person Steve Padmore    schedule 11.03.2015