EF6 с TransactionScope — IsolationLevel.ReadUncommitted, но сначала получен ReadCommitted

При использовании EF для случая обновления из запроса на MSSQL 2008 возникает проблема с производительностью и блокировкой. Поэтому я установил уровень изоляции транзакций ReadUncommitted, надеясь решить эту проблему, вот так:

До

using (MyEntities db = new MyEntities())
{
    // large dataset
    var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact;
    for (var item in data)
          item.Flag = 0;

    // Probably db lock      
    db.SaveChanges(); 
}

После

using (var scope =
    new TransactionScope(TransactionScopeOption.RequiresNew,
    new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
{
    using (MyEntities db = new MyEntities())
    {
        // large dataset but with NOLOCK
        var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact;
        for (var item in data)
              item.Flag = 0;

        // Try avoid db lock      
        db.SaveChanges();
    }
}

Мы используем SQL profiler для трассировки. Тем не менее, эти сценарии приведены в порядок (Ожидайте чтение-неподтвержденное для 1-го сценария.)

Аудит входа

set transaction isolation level read committed

SP:StmtStarting

SELECT 
 [Extent1].[ContactId] AS [ContactId], 
 [Extent1].[MemberId] AS [MemberId], 
FROM [dbo].[Contact] AS [Extent1]
WHERE [Extent1].[MemberId] = @p__linq__0

Аудит входа

set transaction isolation level read uncommitted

Хотя я мог бы повторно отправить этот запрос и сделать его в правильном порядке (покажет read-uncommitted для следующих запросов, тот же SPID), мне интересно, почему он отправил команду чтения-неподтвержденного после команды чтения-фиксации и как это исправить с помощью используя EF и TransactionScope? Спасибо.


person nwpie    schedule 20.01.2017    source источник
comment
Можете ли вы также добавить события, связанные с транзакциями, в профилировщик? (начать транс, откатить, зафиксировать). Было бы интересно узнать, действительно ли создается какая-либо транзакция, и если да, то когда.   -  person Alexei - check Codidact    schedule 20.01.2017
comment
Можете ли вы также указать проблему с производительностью более подробно? (выбор занимает слишком много времени, другие запросы блокируются этим запросом и т. д.). Чтение незафиксированных обычно является плохим вариантом и должен быть последним вариантом.   -  person Alexei - check Codidact    schedule 20.01.2017
comment
Какой подход EF вы используете? Является ли «myEntities» DbContext или ObjectContext?   -  person Patrick    schedule 20.01.2017
comment
Вы должны позвонить TransactionScope.Complete(). Это не должно влиять на то, что вы видите, и вы все равно не можете откатить SELECT, но это неплохо. Также обратите внимание, что если вы не установите уровень изоляции, вы получите тот уровень транзакции, который последний раз применялся к вашему соединению в пуле. Если вы никогда ничего не делаете с уровнем изоляции, это будет значение по умолчанию read committed, но в противном случае это может быть что угодно. См. здесь .   -  person Jeroen Mostert    schedule 20.01.2017
comment
@Alexei, myEntities - это DbContext. На самом деле у меня возникла проблема с блокировкой базы данных обновления (обновление по выбору), потому что набор данных запроса был бы большим результатом от Contact, и его можно было бы избежать с помощью незафиксированного чтения, например с (nolock).   -  person nwpie    schedule 20.01.2017
comment
@Jeroen, я обновил свой пост. Наверное, обозначить проблему. Спасибо за совет.   -  person nwpie    schedule 20.01.2017
comment
Благодаря этому сообщению я протестировал свое собственное приложение, которое работает в EF 6 ObjectContext, и столкнулся с той же проблемой: запрос, выполняемый в TransactionScope (уровень изоляции = ReadUncommited), выполняется как ReadCommited, но все последующие запросы выполняются как ReadUncommited.   -  person Patrick    schedule 20.01.2017
comment
Я думаю, что нашел ответ здесь: SQL Server: утечки уровня изоляции через объединенные соединения.   -  person Patrick    schedule 20.01.2017
comment
@ Патрик, точно. Похоже, что ReadCommitted — это поведение по умолчанию, и его уровень изоляции можно изменить после 2-го запроса в пуле соединений.   -  person nwpie    schedule 21.01.2017


Ответы (3)


Согласно следующему примечанию в документации ADO.NET Изоляция моментальных снимков в SQL Server, уровень изоляции не привязан к области действия транзакции, пока базовое соединение объединено в пул:

Если соединение объединено в пул, сброс его уровня изоляции не приводит к сбросу уровня изоляции на сервере. В результате последующие соединения, использующие одно и то же внутреннее соединение из пула, начинаются с уровнями изоляции, установленными на уровне соединения из пула. Альтернативой отключению пула соединений является явная установка уровня изоляции для каждого соединения.

Таким образом, я прихожу к выводу, что до SQL Server 2012 установка уровня изоляции на любой другой уровень, кроме ReadCommitted, требует либо включения пула соединений при создании сомнительного SqlConnection, либо явной установки уровня изоляции в каждом соединении, чтобы избежать непредвиденного поведения, включая взаимоблокировки. В качестве альтернативы пул соединений можно очистить, вызвав Метод ClearPool, но, поскольку этот метод не привязан ни к области действия транзакции, ни к базовому соединению, я не думаю, что это уместно, когда несколько соединений выполняются одновременно с одним и тем же объединенным внутренним соединением.

Ссылаясь на сообщение SQL Server 2014 сбрасывает уровень изоляции в форуме SQL и моих собственных тестах, такие обходные пути устарели при использовании SQL Server 2014 и клиентского драйвера с TDS 7.3 или выше.

person Patrick    schedule 23.01.2017

Я думаю, что это отвлекающий маневр, вызванный использованием события входа в систему аудита. Это не показывает момент, когда клиент сообщает серверу «установить уровень изоляции транзакции для чтения незафиксированного». Он показывает вам, какой уровень изоляции будет позже, когда это соединение будет выбрано из пула и использовано повторно.

Я проверяю это, добавляя Pooling=false в строку подключения. Затем вход в систему аудита всегда показывает зафиксированный уровень изоляции транзакции.

Я до сих пор не нашел в SQL Profiler способа увидеть момент, когда EF устанавливает уровень транзакции, или какой-либо явный begin tran.

Я могу подтвердить, что он устанавливается где-то, прочитав и записав уровень:

    const string selectIsolationLevel = @"SELECT CASE transaction_isolation_level  WHEN 0 THEN 'Unspecified'  WHEN 1 THEN 'ReadUncommitted'  WHEN 2 THEN 'ReadCommitted'  WHEN 3 THEN 'Repeatable'  WHEN 4 THEN 'Serializable'  WHEN 5 THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL  FROM sys.dm_exec_sessions  where session_id = @@SPID";

    static void ReadUncommitted()
    {
        using (var scope =
            new TransactionScope(TransactionScopeOption.RequiresNew,
            new TransactionOptions{ IsolationLevel = IsolationLevel.ReadUncommitted }))
        using (myEntities db = new myEntities())
        {
            Console.WriteLine("Read is about to be performed with isolation level {0}", 
                db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()
                );
            var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact; // large results but with nolock

            foreach (var item in data)
                item.Flag = 0;

            //Using Nuget package https://www.nuget.org/packages/Serilog.Sinks.Literate
            //logger = new Serilog.LoggerConfiguration().WriteTo.LiterateConsole().CreateLogger();
            //logger.Information("{@scope}", scope);
            //logger.Information("{@scopeCurrentTransaction}", Transaction.Current);
            //logger.Information("{@dbCurrentTransaction}", db.Database.CurrentTransaction);

            //db.Database.ExecuteSqlCommand("-- about to save");
            db.SaveChanges(); // Try avoid db lock
            //db.Database.ExecuteSqlCommand("-- finished save");
            //scope.Complete();
        }
    }

(Я говорю «вроде», потому что каждое утверждение выполняется в своем сеансе)

Возможно, это долгий способ сказать, да, транзакции EF работают правильно, даже если вы не можете доказать это с помощью Profiler.

person Chris F Carroll    schedule 21.01.2017
comment
paulkiddie.com/2013/03/ предполагает, что нам нужно отслеживать события Stored Proc/RPC. - person Chris F Carroll; 21.01.2017

Я думаю, что лучшим решением является выполнение обновления путем создания прямого запроса (а не выбора и обновления сущности за сущностью). Чтобы работать с объектами, а не с запросами, вы можете использовать EntityFramework.Extended:

db.Contact.Update(C => c.MemberId == 13, c => new Contact { Flag = 0 });

Это должно сгенерировать что-то вроде UPDATE Contact SET Flag = 0 WHERE MemberId = 13, которое намного быстрее, чем ваше текущее решение.

Если я правильно помню, это должно генерировать собственную транзакцию. Если это должно быть выполнено в транзакции с другими запросами, все еще можно использовать TransactionScope (у вас будет две транзакции).

Также уровень изоляции может остаться нетронутым (ReadCommitted).

[ИЗМЕНИТЬ]

Анализ Chris's точно показывает, что происходит. Чтобы сделать его еще более актуальным, следующий код показывает разницу внутри и снаружи TransactionScope:

using (var db = new myEntities())
{
    // this shows ReadCommitted
    Console.WriteLine($"Isolation level outside TransactionScope = {db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()}");
}

using (var scope =
    new TransactionScope(TransactionScopeOption.RequiresNew,
    new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted }))
{
    // this show ReadUncommitted
    Console.WriteLine($"Isolation level inside TransactionScope = {db.Database.SqlQuery(typeof(string), selectIsolationLevel).Cast<string>().First()}");

    using (myEntities db = new myEntities ())
    {
        var data = from _Contact in db.Contact where _Contact.MemberId == 13 select _Contact; // large results but with nolock

        for (var item I data)
              item.Flag = 0;
        db.SaveChanges(); // Try avoid db lock
    }

    // this should be added to actually Commit the transaction. Otherwise it will be rolled back
    scope.Complete();
}

Возвращаясь к настоящей проблеме (возникновению взаимоблокировок), если мы посмотрим, что выводит Profiler в течение всего процесса, мы увидим что-то вроде этого (удалены GOs):

BEGIN TRANSACTION 
SELECT <all columns> FROM Contact 
exec sp_reset_connection

exec sp_executesql N'UPDATE Contact
    SET [Flag] = @0
    WHERE ([Contact] = @1)
    ',N'@0 nvarchar(1000),@1 int',@0=N'1',@1=1

-- lots and lots of other UPDATEs like above

-- or ROLLBACK if scope.Complete(); is missed
COMMIT

Это имеет два недостатка:

  1. Много повторных обращений — к базе данных выдается много запросов, что увеличивает нагрузку на ядро ​​базы данных, а также занимает гораздо больше времени для клиента.

  2. Длинная транзакция: следует избегать длинных транзакций в качестве ориентира минимизация взаимоблокировок

Таким образом, предлагаемое решение должно работать лучше в вашем конкретном случае (простое обновление).

В более сложных случаях может потребоваться изменение уровня изоляции.

Я думаю, что если кто-то имеет дело с большой обработкой данных (выбрать миллионы, сделать что-то, обновить и т. д.), хранимая процедура может быть решением, поскольку все выполняется на стороне сервера.

person Alexei - check Codidact    schedule 20.01.2017
comment
Это не отвечает на вопрос, почему область транзакции не устанавливает уровень изоляции для транзакции, где он должен быть. По умолчанию все операции базы данных генерируют неявную транзакцию, которая использует уровень транзакции базы данных по умолчанию. - person Patrick; 20.01.2017
comment
@ Патрик - это правильно, но я предпочел решить реальную проблему (после редактирования стало ясно), чем сам вопрос. См. раздел проблема XY. - person Alexei - check Codidact; 20.01.2017
comment
Установка области транзакции вручную является распространенным шаблоном и, похоже, работает неправильно в Entity Framework. Поэтому, пожалуйста, ответьте на вопрос. - person Patrick; 20.01.2017