实体框架死锁

时间:2014-09-29 14:29:52

标签: c# sql-server entity-framework

我遇到了一直在处理的特定实现问题。 我有一个基本方法,它创建一个新的上下文,查询一个表并从表中获取“LastNumberUsed”,在最终递增和写回之前对这个数字执行一些基本检查 - 所有这些都在一个事务中。

我编写了一个基本的测试应用程序,它使用Parallel.For执行此方法5次。 使用Isolation.Serialization我发现在运行此代码时会出现很多死锁错误。 我已经阅读了这个主题,并尝试将隔离级别更改为快照。我不再遇到死锁,而是发现我遇到隔离更新冲突错误。

我真的不知所措。每个事务需要大约0.009秒才能完成,所以我一直在想着将代码包装在try..catch中,检查死锁错误并再次运行,但这感觉就像一个混乱的解决方案。

是否有人对如何处理这个问题有任何想法(或最好的经验)?

我已经创建了一个控制台应用程序来演示这个。 在程序主目录中,我运行以下代码:

    Parallel.For(0, totalRequests, 
          x => TestContract(x, contractId, incrementBy, maxRetries));

方法TestContract看起来像这样:

//Define the context
using (var context = new Entities())
{
    //Define a new transaction
    var options = new TransactionOptions {IsolationLevel = IsolationLevel.Serializable};
    using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
    {
        //Get the contract details
        var contract = (
            from c in context.ContractRanges
            where c.ContractId == contractId
            select c).FirstOrDefault();

        //Simulate activity
        Threading.Thread.sleep(50);

        //Increment the contract number
        contract.Number++;

        //Save the changes made to the context
        context.SaveChanges();

        //Complete the scope
        scope.Complete();
    }
}
    }

1 个答案:

答案 0 :(得分:40)

暂时搁置隔离级别,让我们关注您的代码正在做的事情:

您正在运行5个并行任务,呼叫TestContract为所有人传递相同的contractId,对吗?

TestContract中,您通过contract获取id,然后使用它进行处理,然后增加合同的Number属性。

所有这些都在交易范围内。

为什么会死锁?

为了理解您遇到僵局的原因,了解Serializable隔离级别的含义非常重要。

documentation of SQL Server Isolation LevelsSerializable强调我的)说了以下内容:

  
      
  • 语句无法读取已修改但尚未由其他事务提交的数据。
  •   
  • 在当前交易完成之前,没有其他交易可以修改当前交易所读取的数据。
  •   
  • 其他事务无法插入新行,其键值将落在当前任何语句读取的键范围内   交易,直到当前交易完成。
  •   
     

范围锁定位于与之匹配的键值范围内   搜索事务中执行的每个语句的条件。这个   阻止其他事务更新或插入任何行   有资格获得当前执行的任何陈述   交易。这意味着如果事务中的任何语句   第二次执行,他们将读取相同的行集。该   范围锁保持到事务完成。这是最多的   限制隔离级别因为它锁定了整个范围   密钥并保持锁定,直到事务完成。因为   并发性较低,仅在必要时使用此选项。这个选项   与在所有SELECT中的所有表上设置HOLDLOCK具有相同的效果   交易中的陈述。

回到您的代码,为了这个示例,我们假设您只有两个并行运行的任务,TaskATaskB contractId=123,所有在Serializable隔离级别的交易下。

让我们尝试描述此执行中的代码:

  • TaskA Starts
  • TaskB Starts
  • TaskA创建具有可序列化隔离级别的Transaction 1234
  • TaskB使用Serializable Isolation Level
  • 创建Transaction 5678
  • TaskA创建SELECT * FROM ContractRanges WHERE ContractId = 123。 在此刻。 SQL Server将ContractRanges表放在ContractId = 123的行中,以防止其他事务变更该数据。
  • TaskB生成相同的SELECT语句,并在lock表的ContractId = 123行中放置ContractRanges

因此,此时,我们在同一行上有两个锁,每个锁用于您创建的每个事务。

  • TaskA然后递增合同的Number
  • TaskB增加合同的Number属性

  • TaskA调用SaveChanges,然后尝试提交事务。

因此,当您尝试提交事务1234时,我们尝试修改具有由事务Number创建的锁的行中的5678值,因此,SQL Server开始等待lock被释放,以便按照您的要求提交交易。

  • TaskB,然后,也会调用SaveChanges,就像TaskA一样,它正在尝试增加合同Number 123。在这种情况下,它会在lock的事务1234创建的该行上找到TaskA

现在我们有来自1234的事务TaskA等待来自事务5678的锁定被释放等待锁定的事务5678来自交易1234即将发布。这意味着我们处于僵局,因为任何事务都无法完成,因为它们互相阻塞。

当SQL Server识别出它处于死锁状态时,它会选择其中一个事务作为victim,将其删除并允许另一个事务继续。

回到隔离级别,如果你真的需要Serializable,我没有足够的详细信息来告诉我你要做什么,但你很有可能不需要它。 Serializable是最安全和最严格的隔离级别,它通过牺牲并发性来实现,就像我们所看到的那样。

如果您真的需要Serializable保证,您真的不应该同时尝试更新同一合同的Number

Snapshot Isolation替代

你说:

  

我已经阅读了这个主题,并尝试将隔离级别更改为快照。我不再遇到死锁,而是发现我遇到隔离更新冲突错误。

如果您选择使用快照隔离,那就是您想要的行为。这是因为Snapshot使用了 Optimistic Concurrency 模型。

以下是它在同一MSDN文档中的定义(再次强调我的):

  

指定事务中任何语句读取的数据都是   事务上一致的数据版本存在于   交易开始。交易只能识别数据   在事务开始之前提交的修改。   其他交易在开始后进行的数据修改   当前事务对于在中执行的语句不可见   当前交易。效果就像是在a中的陈述   transaction获取已存在的已提交数据的快照   交易开始。

     

除了恢复数据库之外, SNAPSHOT事务在读取数据时不会请求锁定。读取数据的SNAPSHOT事务不会阻止其他事务写入数据。写入数据的事务不会阻止SNAPSHOT事务读取数据。

     

在数据库恢复的回滚阶段,   如果尝试,SNAPSHOT事务将请求锁定   读取由正在滚动的另一个事务锁定的数据   背部。 SNAPSHOT事务被阻止,直到该事务具有   被退了回来。锁之后立即释放   理所当然的。

     

必须将ALLOW_SNAPSHOT_ISOLATION数据库选项设置为   在开始使用SNAPSHOT隔离的事务之前打开   水平。如果使用SNAPSHOT隔离级别的事务访问   在多个数据库中的数据,ALLOW_SNAPSHOT_ISOLATION必须设置为ON   在每个数据库中。

     

无法将事务设置为SNAPSHOT隔离   从另一个隔离级别开始的级别;这样做会导致   要中止的交易。如果交易在SNAPSHOT中开始   隔离级别,您可以将其更改为另一个隔离级别   回到SNAPSHOT。事务在第一次访问时启动   数据。

     

在SNAPSHOT隔离级别下运行的事务可以查看   该交易所做的更改。例如,如果是交易   对表执行UPDATE,然后发出SELECT语句   针对同一个表,修改后的数据将包含在   结果集。

让我们尝试描述代码在快照隔离下执行时的情况:

  • 我们假设合约Number的{​​{1}}的初始值为2
  • TaskA Starts
  • TaskB Starts
  • TaskA创建具有快照隔离级别的123
  • TaskB使用快照隔离级别
  • 创建Transaction 1234

在两个快照中,Transaction 5678用于合同Number = 2

  • TaskA创建123。由于我们在SELECT * FROM ContractRanges WHERE ContractId = 123隔离下运行,因此没有Snapshot

  • 任务B发出相同的locks声明,并且放置任何SELECT

  • TaskA然后将合同的locks增加到Number
  • TaskB将合同的3属性增加到Number

  • TaskA调用3,这反过来又导致SQL Server比较创建事务时创建的快照和数据库的当前状态以及在此下进行的未提交的更改。这笔交易。由于它没有发现任何冲突,因此它会提交事务,现在SaveChanges在数据库中的值为Number

  • 3,然后,还调用TaskB,并尝试提交其事务。当SQL Server将事务快照值与当前在数据库中的事务快照值进行比较时,它会发现冲突。在快照中,SaveChanges的值为Number,现在其值为2。然后,它会抛出3

同样,没有死锁,但这次Update Exception失败,因为TaskB突变了TaskA中也使用过的数据。

如何解决此问题

现在我们已经介绍了在TaskBSerializable隔离级别下运行代码时代码的内容,您可以对Snapshot进行操作。

嗯,你应该考虑的第一件事是,你是否真的有意义地同时改变同一个fix记录。这是我在你的代码中看到的第一个大气味,我会首先尝试理解它。您可能需要与您的企业讨论这个问题,以了解他们是否真的需要合同上的这种并发性。

假设你确实需要同时发生这种情况,正如我们所看到的那样,你无法像Contract那样真正使用Serializable。所以,我们留下deadlocks隔离。

现在,当你发现Snapshot时,你需要处理的事情取决于你和你的公司决定。

例如,处理它的一种方法是简单地委托用户通过向用户显示错误消息来决定做什么,通知他们尝试更改的数据已经被修改并询问他们是否想要刷新屏幕以获取最新版本的数据,如果需要,尝试再次执行相同的操作。

如果不是这种情况,并且您可以重试,则另一个选项是您在代码中具有重试逻辑,该逻辑会在抛出OptmisticConcurrencyException时重试执行操作。这是基于这样的假设:在第二次,不会发生变异相同数据的并发事务,现在操作将成功。