Как избежать исключения PlatformNotSupportedException при использовании пула соединений TransactionScope и SQL в .NET Core / .NET 5+

В веб-службе я запрашиваю базу данных SQL Server 2016. Используя .NET TransactionScope следующим образом, чтобы сохранить управление транзакциями на моем уровне обслуживания, но запросы / команды данных в моем коде уровня данных (классы «хранилища»), у нас есть несколько мест, которые следуют этому шаблону:

      using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    bool needsInsert = await store1.Exists(request.id);
    if (needsInsert) mainRowsUpdatedCount = await store2.Insert(request);
    transaction.Complete();
}

Каждый из этих методов «store» следует этому шаблону (с использованием Dapper, хотя я подозреваю, что это не имеет значения):

      const string query = @"SELECT ...";                        // or INSERT or MERGE as the case may be
using IDbConnection connection = new SqlConnection(ConnectionString.Value);
return await connection.QueryAsync<T>(query, new { ... }); // or connection.ExecuteAsync as the case may be

Это отлично работает для большинства вызовов, но иногда я получаю следующее (хотя и довольно редко):

System.PlatformNotSupportedException: эта платформа не поддерживает распределенные транзакции.

Так может быть так, что в приведенном выше примере store1.Exists запускается, получает соединение, включает его в транзакцию, выполняет свой запрос, закрывается, а иногда до того, как store2.Insert может быть запущен, какой-то другой несвязанный поток получает такое же соединение от пул соединений, в котором уже есть открытая транзакция, пытается выполнить запрос и, таким образом, выдает исключение PlatformNotSupportedException, поскольку .NET Core (или .NET 5+) не поддерживает распределенные транзакции?

Если да, то как я могу это преодолеть, не передавая свои связи?

Если нет, что еще может вызвать это исключение?

1 ответ

Я столкнулся с той же проблемой (тоже довольно редко) и в конце концов пришел к выводу, что это, скорее всего, ошибка в базовых библиотеках .NET Core (либо SqlClient, либо Transaction).

Насколько я понимаю, существует разница между транзакцией .NET и локальной транзакцией SQL. Создавая, вы эффективно запускаете новую транзакцию .NET, которая всегда доступна через . Звонок на вновь созданном внутренне обнаруживает эту внешнюю транзакцию .NET и запрашивает внутренний пул соединений - где хранятся фактические соединения SQL - для любого подходящего существующего соединения (то есть с той же самой строкой соединения), которое уже связано с этим окружающим .NET сделка. Если один из них доступен (то есть свободен / простаивает), он напрямую использует его, не переходя в распределенную транзакцию.

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

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

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

В общем, я по-прежнему считаю, что это какая-то ошибка, спрятанная глубоко в спагетти-коде транзакций .NET. ;) Моя проблема в том, что я встречаю исключение только в редких случаях в продакшене, я просто не могу воспроизвести его с помощью модульного теста или чего-то еще.

Другие вопросы по тегам