【问题标题】:EF6 with TransactionScope - IsolationLevel.ReadUncommitted but got ReadCommitted first带有 TransactionScope 的 EF6 - IsolationLevel.ReadUncommitted 但首先获得了 ReadCommitted
【发布时间】:2017-01-20 08:49:54
【问题描述】:

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-committed 命令之后发送了 read-uncommitted 命令以及如何使用 EF 和 TransactionScope 进行修复?谢谢。

【问题讨论】:

  • 你也可以在分析器中添加事务相关的事件吗? (开始传输、回滚、提交)。想知道是否实际创建了任何交易,如果是,何时创建。
  • 能否更详细地指出性能问题? (选择需要太多,其他查询被此查询阻止等)。未提交读取通常是一个糟糕的选择,应该是最后一个选择。
  • 您使用哪种 EF 方法? 'myEntities' 是 DbContext 还是 ObjectContext?
  • 您应该致电TransactionScope.Complete()。这应该不会影响您所看到的内容,并且无论如何您都无法回滚SELECT,但这是一件好事。另请注意,如果您不设置隔离级别,您将获得上次应用于池连接的任何事务级别。如果您从不使用隔离级别做任何事情,这将是默认的read committed,否则它可能是任何事情。见here
  • @Alexei,myEntities 是一个 DbContext。实际上我遇到了更新数据库锁定问题(通过选择更新),因为查询数据集将是 Contact 的一个大结果,并且可以通过未提交的读取来避免,例如使用 (nolock)。

标签: c# sql-server entity-framework-6 transactionscope read-uncommitted


【解决方案1】:

我认为这是依赖审计登录事件引起的红鲱鱼。这显示客户端告诉服务器“设置事务隔离级别读取未提交”的时刻。它会向您展示稍后的隔离级别,当该连接从池中被挑选出来并被重用时。

我通过将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 事务也可以正常工作。

【讨论】:

【解决方案2】:

根据 ADO.NET 文档Snapshot Isolation in SQL Server 中的以下注释,只要底层连接被池化,Isolation Level 就不会绑定到 Transaction Scope:

如果连接是池化的,则重置其隔离级别不会 重置服务器的隔离级别。结果,随后 使用相同池化内部连接的连接以它们的 隔离级别设置为池连接的隔离级别。替代 关闭连接池就是设置隔离级别 明确地为每个连接。

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

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

【讨论】:

    【解决方案3】:

    我认为更好的解决方案是通过生成直接查询(而不是逐个实体选择和更新实体)来执行更新。为了使用对象而不是查询,您可以使用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. 长事务 - 暂时应避免长事务minimizing deadlocks

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

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

    我认为,如果要处理大量数据(选择数百万、做某事、更新等),存储过程可能是解决方案,因为一切都在服务器端执行。

    【讨论】:

    • 这并没有回答为什么事务范围没有在它应该在的事务上设置隔离级别的问题。默认情况下,所有数据库操作都会生成一个使用数据库默认事务级别的隐式事务。
    • @Patrick - 这是正确的,但我更愿意解决实际问题(编辑后,它变得清晰)而不是问题本身。见XY problem
    • 手动设置事务范围是一种常见的模式,它在实体框架下似乎工作不正确。所以请回答这个问题。
    猜你喜欢
    • 2011-10-29
    • 2022-08-10
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多