Entity Framework死锁和并发问题

9
我们在使用Entity Framework 6和SqlSever 2012的数据库优先模型中广泛使用Entity Framework。我们有一些相当长时间运行的进程(10多秒),每个进程都创建一个相同类型的对象,并使用不同的数据,在其创建过程中,这些对象都会使用Entity Framework在数据库中写入和删除数据。到目前为止还不错。为了提高应用程序的性能,我们正在尝试并行运行这些操作,因此我们使用“Task”构造来实现如下:
Private Async Function LongRunningProcessAsync(data As SomeData) As Task(Of LongRunningProcessResult)
    Return Await Task.Factory.StartNew(Of LongRunningProcessResult)(Function()
                                                       Return Processor.DoWork(data)
                                                     End Function)             
End Function

我们运行10个这样的任务,然后使用 Task.WaitAll 等待它们全部完成。

Class Processor
    Public Function DoWork(data As SomeData) As LongRunningProcessResult
        Using context as new dbContext() 
           ' lots of database calls 
           context.saveChanges()
        end Using

        ' call to sub which creates a new db context and does some stuff
        doOtherWork()

        ' final call to delete temporary database data
        using yetAnotherContext as new dbContext()
            Dim entity = yetAnotherContext.temporaryData.single(Function(t) t.id = me.Id)
            yetAnotherContext.temporaryDataA.removeAll(entity.temporaryDataA)
            yetAnotherContext.temporaryDataB.removeAll(entity.temporaryDataB)
            yetAnotherContext.temporaryData.remove(entity)

            ' dbUpdateExecption Thrown here
            yetAnotherContext.SaveChanges()
        end using
    End Function
End Class

这通常可以正常运行,约90%的时间内都没有问题,但另外10%的时间会导致数据库服务器死锁,出现内部死锁异常。

所有处理器都使用相同的表,但进程之间绝对不共享数据(也不依赖于相同的FK行),并且创建它们自己的entityframework上下文而没有任何共享交互。

通过审查Sql Server实例的性能行为,我们发现在每次成功查询之间有大量非常短暂的锁定获取和释放。这导致最终产生了死锁链:

Lock:Deadlock Chain Deadlock Chain SPID = 80 (e413fffd02c3)         
Lock:Deadlock Chain Deadlock Chain SPID = 73 (e413fffd02c3)     
Lock:Deadlock Chain Deadlock Chain SPID = 60 (6cb508d3484c) 

这些锁本身属于KEY类型,而死锁查询都是针对同一张表的,但使用不同形式的键:

exec sp_executesql N'DELETE [dbo].[temporaryData]
WHERE ([Id] = @0)',N'@0 int',@0=123

我们对实体框架相对较新,无法确定似乎是过度范围锁定的根本原因(我无法通过SQL分析器确定精确锁定的行)。

编辑:deadlock.xdl

编辑2:在每个删除语句后调用saveChanges会消除死锁,但仍不太明白为什么死锁会发生。


对于所有的isolationlevel="read committed (2)" - user2732663
看起来我输了那个赌注。 :) 你能把XDL文件放在某个地方进行分析吗? - Ben Thul
@BenThul 请查看附加的 xdl 文件,谢谢。 - user2732663
嗯... 这很奇怪。不同的线程是否有可能尝试在一个事务中删除多个单例,并尝试删除相同的ID?根据您使用的 SQL Server 版本,您可以使用扩展事件肯定或否认 a) 并很可能也能确认 b)。 - Ben Thul
我已确认它们是不同的ID,删除操作似乎锁定了整个表。 - user2732663
显示剩余4条评论
2个回答

10

您似乎成为了“锁升级”的受害者。

为提高性能,Sql Server(以及所有现代DB引擎)会将许多低级别的细粒度锁转换为少量高级别的粗粒度锁。在您的情况下,当超出阈值后,它从行级锁转变为完整表锁。您可以通过以下几种方式解决此问题:

  1. 一种解决方法是调用SaveChanges(),您已经这样做了。 这将更快地释放锁定,防止发生锁升级, 因为低锁定计数=较不可能达到升级阈值。
  2. 您还可以将隔离级别更改为未提交读取, 通过允许脏读取来降低锁定数量, 这也可以防止发生锁升级。
  3. 最后,您应该能够使用SET命令发送ALTER TABLE命令 (LOCK_ESCALATION = {AUTO|TABLE|DISABLE})。但是, 即使禁用了锁定升级,表级锁定仍然是可能的。 MSDN提到一个扫描没有聚集索引的表, 在可串行化隔离级别下仍有可能出现表级锁定。 请参见这里: https://msdn.microsoft.com/en-us/library/ms190273(v=sql.110).aspx

在您的情况下,您已经采取的调用SaveChanges()的解决方案, 使事务提交且锁定被释放是首选项。


0
死锁是指两个或多个进程想要获取相同的资源,但每个进程以不同的顺序获取其资源。避免死锁的(相当复杂)方法是按照一定的顺序获取资源。例如,在更新一堆记录(相同类型的记录)时,按照它们的主键顺序更新记录。
一个更简单的方法是指定事务的根 - 一些父记录,并始终首先更新该记录。因此,如果将表分成逻辑块 - DDD 将这些聚合称为聚合,并且有一个负责更新每个聚合的单个代码区域,则特定聚合的代码始终可以在操作聚合中的子表之前首先更新聚合的根(现在可以按任意顺序进行操作)。
通过这种方式,对同一聚合(例如客户聚合,客户 ID 为 123)的两个或多个操作都将尝试更新客户聚合根。其中一个将获胜,另一个将被阻止(但不会死锁),直到获胜者提交其更改。这种方法还将帮助您确保聚合中的不变量得到维护,而且根上的版本号/时间戳也将允许您检查(否则隐藏的)更新。

EF 可能无法帮助您 - 您可能需要先更新根并保存更改,以确保 EF 不决定更新的顺序并将根放在最后 - 这将失去目的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接