带有 TransactionScope 的 EF6 - IsolationLevel.ReadUncommitted 但首先获得 ReadCommitted

EF6 with TransactionScope - IsolationLevel.ReadUncommitted but got ReadCommitted first

MSSQL 2008 上使用 EF 进行查询更新时存在性能和锁定问题。所以我设置了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来追踪。但是,按顺序获取这些脚本, (预计第一个脚本未提交读取。)

审核登录

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),但我想知道为什么它发送了 read-读取提交命令后未提交的命令以及如何使用 EF 和 TransactionScope 修复?谢谢。

我认为更好的解决方案是通过生成直接查询来执行更新(而不是 selection 并逐个更新实体)。为了使用对象而不是查询,您可以使用 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'的分析准确地显示了发生的事情。为了使其更具相关性,以下代码显示了 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. 许多 round-trips - 对数据库发出了很多查询,这给数据库引擎带来了更大的压力,并且客户端也需要更长的时间

  2. 长交易 - 应避免长交易作为 minimizing deadlocks

  3. 的暂定

因此,建议的解决方案应该更适合您的特定情况(简单更新)。

在更复杂的情况下,可能需要更改隔离级别。

我认为,如果要处理大量数据(select 数百万、做某事、更新回来等),存储过程可能是解决方案,因为所有内容都会执行 server-side。

我认为这是依赖审计登录事件引起的转移注意力。这是 而不是 显示客户端告诉服务器 'set transaction isolation level read uncommitted' 的时刻。当从连接池中取出并重新使用该连接时,它向您展示稍后的隔离级别。

我通过将 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();
        }
    }

(我说“有点”是因为每个语句 运行 在它们自己的会话中)

也许这是一个很长的说法,是的,即使您无法通过 Profiler 证明它,EF 事务也能正常工作。

根据 ADO.NET 文档 Snapshot Isolation in SQL Server 中的以下注释,只要基础连接被合并,隔离级别就不会绑定到事务范围:

If a connection is pooled, resetting its isolation level does not reset the isolation level at the server. As a result, subsequent connections that use the same pooled inner connection start with their isolation levels set to that of the pooled connection. An alternative to turning off connection pooling is to set the isolation level explicitly for each connection.

因此我得出结论,在 SQL Server 2012 之前,将隔离设置为 ReadCommitted 以外的任何其他级别都需要在创建有问题的 SqlConnection 时打开连接池,或者将隔离级别设置为每个连接明确地避免意外行为,包括死锁。或者,可以通过调用 ClearPool Method 来清除连接池,但是由于此方法既未绑定到事务范围也未绑定到底层连接,因此我认为当多个连接 运行 同时反对时它是不合适的相同的池化内部连接。

参考 SQL 论坛中的 post SQL Server 2014 reseting isolation level 和我自己的测试,当使用 SQL Server 2014 和带有 TDS 7.3 的客户端驱动程序时,此类解决方法已过时或更高。