Entity Framework и Transactionscope не возвращают уровень изоляции после удаления Transactionscope

Я немного борюсь с областями транзакций и инфраструктурой сущностей.

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

Однако проблема заключается в том, что при размещении области транзакции изоляция по-прежнему остается в соединении, что вызывает немало проблем.

Я пробовал все типы вариаций, но с тем же результатом; уровень изоляции сохраняется за пределами транзакции.

Есть ли кто-нибудь, кто может объяснить это поведение для меня или может объяснить, что я делаю неправильно?

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

Вот пример кода, который иллюстрирует проблему:

using (var context = new MyContext())
{
    context.Database.Connection.Open();

    //Sets the connection to default read snapshot
    using (var command = context.Database.Connection.CreateCommand())
    {
        command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
        command.ExecuteNonQuery();
    }

    //Executes a DBCC USEROPTIONS to print the current connection information and this shows snapshot
    PrintDBCCoptions(context.Database.Connection);

    //Executes a query
    var result = context.MatchTypes.ToArray();

    //Executes a DBCC USEROPTIONS to print the current connection information and this still shows snapshot
    PrintDBCCoptions(context.Database.Connection);

    using (var scope = new TransactionScope(TransactionScopeOption.Required,
        new TransactionOptions()
        {
            IsolationLevel = IsolationLevel.ReadCommitted //Also tried ReadUncommitted with the same result
        }))
    {
        //Executes a DBCC USEROPTIONS to print the current connection information and this still shows snapshot
        //(This is ok, since the actual new query with the transactionscope isn't executed yet)
        PrintDBCCoptions(context.Database.Connection);
        result = context.MatchTypes.ToArray();
        //Executes a DBCC USEROPTIONS to print the current connection information and this has now changed to read committed as expected                    
        PrintDBCCoptions(context.Database.Connection);
        scope.Complete(); //tested both with and without
    }

    //Executes a DBCC USEROPTIONS to print the current connection information and this is still read committed
    //(I can find this ok too, since no command has been executed outside the transaction scope)
    PrintDBCCoptions(context.Database.Connection);
    result = context.MatchTypes.ToArray();

    //Executes a DBCC USEROPTIONS to print the current connection information and this is still read committed
    //THIS ONE is the one I don't expect! I expected that the islation level of my connection should revert here
    PrintDBCCoptions(context.Database.Connection);
}

Ответ 1

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

Существует несколько причин, по которым моя проблема зависит от среды.

Версия сервера базы данных:

Прежде всего, результат операций зависит от версии SQL Server, которую вы используете (протестирован на SQL Server 2012 и SQL Server 2014).

SQL Server 2012

В SQL Server 2012 последний уровень изоляции будет следовать за соединением при последующих операциях, даже если он будет выпущен обратно в пул соединений и возвращен обратно из других потоков/действий. На практике; это означает, что если вы в каком-либо потоке/действии устанавливаете уровень изоляции для чтения без фиксации с использованием транзакции, соединение сохраняет это, пока другая область транзакций не установит его на другой уровень изоляции (или выполнив команду SET TRANSACTION ISOLATION LEVEL на подключение). Нехорошо, вы можете внезапно получить грязные чтения, не зная об этом.

Например:

Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());

using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                                        new TransactionOptions 
                                        { 
                                            IsolationLevel = IsolationLevel.ReadUncommitted 
                                        }))
{
    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                                        .Select(mt => mt.LastUpdated).First());
    scope.Complete(); //tested both with and without
}

Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());

В этом примере первая команда EF будет запускаться с базой данных по умолчанию, а внутри области транзакций будет работать с ReadUncommitted, а третья - с ReadUncommitted.

SQL Server 2014

В SQL Server 2014, с другой стороны, каждый раз, когда соединение получается из пула соединений, процедура sp_reset_connection (похоже, что это так) в любом случае устанавливает уровень изоляции по умолчанию в базе данных, ДАЖЕ, если соединение извлекается из одной и той же области транзакции. На практике; это означает, что если у вас есть область транзакций, в которой вы выполняете две последующие команды, только первый получит уровень изоляции области транзакции. Также не хорошо; вы получите (на основе уровня изоляции по умолчанию в базе данных) либо получите данные о блокировке или моментальном снимке.

Например:

Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());

using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                                        new TransactionOptions 
                                        { 
                                            IsolationLevel = IsolationLevel.ReadUncommitted 
                                        }))
{
    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                             .Select(mt => mt.LastUpdated).First());
    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                             .Select(mt => mt.LastUpdated).First());
    scope.Complete(); 
}

В этом примере первая команда EF будет запускаться с базой данных по умолчанию, первая из транзакций будет выполняться с помощью ReadUncommitted, а вторая в рамках области будет снова запущена по умолчанию для базы данных.

Проблема с открытием вручную:

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

Использование Database.BeginTransaction:

По какой-то причине логика Database.BeginTransaction для Entity Framework, похоже, работает в обеих базах данных, которая в порядке, но в нашем коде мы работаем с двумя разными базами данных, а затем нам нужны области транзакций.

Вывод:

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

Но факт остается фактом, нам нужно, чтобы это работало в нашем коде. Имея дело с утомительной поддержкой в ​​MS в последнее время не с тем большим результатом, я сначала найду обходное решение, которое работает для нас. Затем я расскажу о своих выводах с помощью Connect и надеюсь, что Microsoft сделает некоторые действия в области обработки транзакций и соединений.

Решение:

Решение (насколько я пришел) выглядит следующим образом.

Вот требования, которые это решение будет иметь: 1. База данных ДОЛЖНА должна быть ЗАВЕРШЕНА на уровне изоляции из-за других приложений, которые работают с той же базой данных, которая требует этого, мы не можем использовать значения READ COMMITTED SNAPSHOT по умолчанию в базе данных 2. Наше приложение MUST имеет значение по умолчанию для уровня изоляции SNAPSHOT - Это можно решить, используя SET TRANSACTION ISOLATIONLEVEL SNAPSHOT 3. Если есть область транзакций, нам нужно соблюдать уровень изоляции для этого

Таким образом, основываясь на этих критериях, решение будет таким:

В конструкторе контекста я регистрируюсь в событии StateChange, где я в свою очередь, когда состояние изменено на Open, и нет активной транзакции по умолчанию для уровня изоляции для моментального снимка с использованием классического ADO.NET. Если используется область транзакции, мы должны соблюдать ее настройки, запустив SET TRANSACTION ISOLATIONLEVEL на основе настроек здесь (чтобы ограничить наш собственный код, мы разрешим только IsolationLevel ReadCommitted, ReadUncommitted и Snapshot). Что касается транзакций, созданных Database.BeginTransaction в контексте, кажется, что это соблюдается, так как это так, мы не делаем никаких специальных действий с этими типами транзакций.

Вот код в контексте:

public MyContext()
{
    Database.Connection.StateChange += OnStateChange;
}

protected override void Dispose(bool disposing)
{
    if(!_disposed)
    {
        Database.Connection.StateChange -= OnStateChange;
    }

    base.Dispose(disposing);
}

private void OnStateChange(object sender, StateChangeEventArgs args)
{
    if (args.CurrentState == ConnectionState.Open && args.OriginalState != ConnectionState.Open)
    {
        using (var command = Database.Connection.CreateCommand())
        {
            if (Transaction.Current == null)
            {
                command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
            }
            else
            {
                switch (Transaction.Current.IsolationLevel)
                {
                    case IsolationLevel.ReadCommitted:
                        command.CommandText = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
                        break;
                    case IsolationLevel.ReadUncommitted:
                        command.CommandText = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
                        break;
                    case IsolationLevel.Snapshot:
                        command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            command.ExecuteNonQuery();
        }
    }
}

Я тестировал этот код как в SQL Server 2012, так и в 2014 году, и, похоже, он работает. Это не самый приятный код, и он имеет свои ограничения (например, для каждого запуска EF всегда выполняется SET TRANSACTION ISOLATIONLEVEL по отношению к базе данных и, таким образом, добавляется дополнительный сетевой трафик.)