SQL Server: утечка уровня изоляции через соединения в пуле

Как показали предыдущие вопросы о переполнении стека (TransactionScope и Connection Pooling и Как SqlConnection управляет IsolationLevel?), уровень изоляции транзакции просачивается через объединенные соединения с SQL Server и ADO.NET (также System.Transactions и EF, поскольку они построены на основе ADO.NET).

Это означает, что в любом приложении может произойти следующая опасная последовательность событий:

  1. Происходит запрос, который требует явной транзакции для обеспечения согласованности данных.
  2. Приходит любой другой запрос, в котором не используется явная транзакция, потому что он выполняет только некритические чтения. Теперь этот запрос будет выполняться как сериализуемый, потенциально вызывая опасные блокировки и взаимоблокировки.

Вопрос: Как лучше всего предотвратить этот сценарий? Действительно ли сейчас требуется везде использовать явные транзакции?

Вот самодостаточная репродукция. Вы увидите, что третий запрос унаследует уровень Serializable от второго запроса.

class Program
{
    static void Main(string[] args)
    {
        RunTest(null);
        RunTest(IsolationLevel.Serializable);
        RunTest(null);
        Console.ReadKey();
    }

    static void RunTest(IsolationLevel? isolationLevel)
    {
        using (var tran = isolationLevel == null ? null : new TransactionScope(0, new TransactionOptions() { IsolationLevel = isolationLevel.Value }))
        using (var conn = new SqlConnection("Data Source=(local); Integrated Security=true; Initial Catalog=master;"))
        {
            conn.Open();

            var cmd = new SqlCommand(@"
select         
        case transaction_isolation_level 
            WHEN 0 THEN 'Unspecified' 
            WHEN 1 THEN 'ReadUncommitted' 
            WHEN 2 THEN 'ReadCommitted' 
            WHEN 3 THEN 'RepeatableRead' 
            WHEN 4 THEN 'Serializable' 
            WHEN 5 THEN 'Snapshot' 
        end as lvl, @@SPID
     from sys.dm_exec_sessions 
    where session_id = @@SPID", conn);

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    Console.WriteLine("Isolation Level = " + reader.GetValue(0) + ", SPID = " + reader.GetValue(1));
                }
            }

            if (tran != null) tran.Complete();
        }
    }
}

Вывод:

Isolation Level = ReadCommitted, SPID = 51
Isolation Level = Serializable, SPID = 51
Isolation Level = Serializable, SPID = 51 //leaked!

person usr    schedule 24.03.2012    source источник


Ответы (4)


В SQL Server 2014, похоже, это было исправлено. При использовании протокола TDS 7.3 или более поздней версии.

При запуске на SQL Server версии 12.0.2000.8 вывод будет следующим:

ReadCommitted
Serializable
ReadCommitted

К сожалению, это изменение не упоминается ни в одной документации, например:

Но это изменение задокументировано на форуме Microsoft.

Обновление 2017-03-08

К сожалению, позже это было исправлено в SQL Server 2014 CU6 и SQL Server 2014 SP1 CU1, поскольку в нем была обнаружена ошибка:

ИСПРАВЛЕНИЕ: уровень изоляции транзакции сбрасывается неправильно при освобождении подключения к SQL Server в SQL Server 2014

Предположим, что вы используете класс TransactionScope в исходном коде на стороне клиента SQL Server и не открываете соединение SQL Server в транзакции явным образом. Когда соединение с SQL Server разрывается, уровень изоляции транзакции сбрасывается неправильно.

Обходной путь

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

Поэтому, чтобы избежать утечки, пропустите фиктивный параметр, как показано ниже.

using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(@"
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = @@SPID
", conn))
{
    conn.Open();
    Console.WriteLine(comm.ExecuteScalar());
}
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(@"
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = @@SPID
", conn))
{
    comm.Parameters.Add("@dummy", SqlDbType.Int).Value = 0;  // see with and without
    conn.Open();
    Console.WriteLine(comm.ExecuteScalar());
}
using (var conn = new SqlConnection(connString))
using (var comm = new SqlCommand(@"
SELECT transaction_isolation_level FROM sys.dm_exec_sessions where session_id = @@SPID
", conn))
{
    conn.Open();
    Console.WriteLine(comm.ExecuteScalar());
}
person Thomas    schedule 01.09.2014
comment
Выглядит отлично. Жду официального подтверждения. Оставьте здесь комментарий, если заметите. Проблема с подключением еще не решена. - person usr; 01.09.2014
comment
Подтверждение команды SQL на форуме MSDN: social.msdn.microsoft.com/Forums/sqlserver/en-US/ - person Thomas; 04.09.2014
comment
В любом случае, какое-то время все еще будут SQL 2005, 2008 и 2012 с большинством бизнес-приложений, но приятно видеть, что транзакции наконец стали транзакционными с точки зрения уровня изоляции. - person Erik Hart; 16.01.2015
comment
Празднование появления Sql2014 может быть преждевременным - см. Здесь: support.microsoft.com/en-us/ кб / 3025845 - person StuartLC; 01.04.2015
comment
Я только что протестировал 12.0.4100 (2014 SP1), и исправление все еще актуально. Третье соединение возвращается к ReadCommitted. - person Nick Jones; 19.06.2015
comment
Я тестировал это на SQL Azure V12, и третье соединение возвращается к ReadCommitted. - person Shane Neuville; 27.08.2015
comment
Я только что протестировал это на SQL Server 2014 Standard SP4 CU2, и третье соединение является сериализуемым, то есть исправления, похоже, нет. - person Richard; 24.10.2016
comment
@Stony исправление также зависит от используемой версии протокола TDS. Для этого требуется, чтобы вы использовали как минимум .NET 4.0 (согласно github.com/Microsoft/azure-docs/blob/, в котором указано, что ADO.NET 4.0 использует TDS 7.3). В freetds.org/userguide/tdshistory.htm говорится, что в 7.3 добавлена ​​поддержка расширенной даты типы времени, которые были добавлены в ADO.NET для 3.5 SP1. - person Mike Dimmick; 13.01.2017
comment
Эта проблема все еще возникает в SQL Server 2016 SP1 CU5 с клиентом .Net 4.6, работающим на Windows Server 2016. - person Mitch; 10.02.2018

Пул соединений вызывает процедуру sp_resetconnection перед перезапуском соединения. Сброс уровня изоляции транзакции не входит в список действий, выполняемых sp_resetconnection. Это могло бы объяснить, почему "сериализуемые" утечки через объединенные соединения.

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

if not exists (
              select  * 
              from    sys.dm_exec_sessions 
              where   session_id = @@SPID 
                      and transaction_isolation_level = 2
              )
    set transaction isolation level read committed

Другой вариант: подключения с другой строкой подключения не используют общий пул подключений. Таким образом, если вы используете другую строку подключения для «сериализуемых» запросов, они не будут совместно использовать пул с запросами «зафиксировано чтение». Простой способ изменить строку подключения - использовать другой логин. Вы также можете добавить случайный вариант, например Persist Security Info=False;.

Наконец, вы можете убедиться, что каждый «сериализуемый» запрос сбрасывает уровень изоляции перед возвратом. Если «сериализуемый» запрос не может быть выполнен, вы можете очистить пул соединений, чтобы удалить испорченное соединение из пула:

SqlConnection.ClearPool(yourSqlConnection);

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

person Andomar    schedule 24.03.2012
comment
Это поведение «по замыслу»: connect.microsoft.com/SQLServer/feedback/details/243527/ - person user423430; 20.07.2012
comment
Принимаю это, потому что это показывает, что такое поведение является преднамеренным. Кажется, нет хорошего решения. - person usr; 29.07.2013
comment
Мы пошли по маршруту строки подключения. Если Transaction.Current не равно null, мы меняем имя приложения. - person Mark Sowul; 14.10.2013
comment
Для меня имеет смысл использовать разные строки подключения для разных уровней изоляции - person Chris F Carroll; 21.01.2017
comment
Примечание. Добавление пробелов в конец строки подключения достаточно, чтобы получить ее из другого пула. Я серьезно думаю об этом подходе: - / - person user2864740; 26.02.2017

Для тех, кто использует EF в .NET, вы можете исправить это для всего приложения, установив другое имя приложения для каждого уровня изоляции (как также указано @Andomar):

//prevent isolationlevel leaks
//https://stackoverflow.com/questions/9851415/sql-server-isolation-level-leaks-across-pooled-connections
public static DataContext CreateContext()
{
    string isolationlevel = Transaction.Current?.IsolationLevel.ToString();
    string connectionString = ConfigurationManager.ConnectionStrings["yourconnection"].ConnectionString;
    connectionString = Regex.Replace(connectionString, "APP=([^;]+)", "App=$1-" + isolationlevel, RegexOptions.IgnoreCase);

    return new DataContext(connectionString);
}

Странно, что это все еще проблема 8 лет спустя ...

person MichaelD    schedule 02.02.2020
comment
Очень креативное решение! - person usr; 02.02.2020
comment
Причина, по которой это работает, заключается в том, что пул соединений для каждой строки соединения, поэтому вы можете изменить что угодно в строке соединения, чтобы получить новый пул. - person Christian Davén; 08.04.2021

Я только что задал вопрос по этой теме и добавил фрагмент кода C #, который может помочь в решении этой проблемы (что означает: изменить уровень изоляции только для одной транзакции).

Изменить уровень изоляции только в отдельных транзакциях ADO.NET

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

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

person Erik Hart    schedule 16.01.2015