我有一个通过 EF 代码优先模型更新的数据库。该数据库包含一个“父”表和 10 或 12 个“子”表。父表和子表之间存在外键关系。
最初,当我想在子表中创建记录时,我会使用 EF 创建父行,获取主键,然后使用 EF 创建子行,将主键用作子表中的外键.
最近我重构了我的模型以使用 TPT 继承。所以现在我有一个基类(对应于我的基表)和子类(对应于我的子表)。所以现在如果我想创建一个子记录,我创建一个子类型的对象,填充所有必需的字段并使用上下文进行保存。我假设(因为它是合乎逻辑的)EF 生成的 SQL 与我手动执行的操作几乎相同:
- 创建父记录。
- 获取新父记录的主键
- 创建子记录
现在的问题是,使用 TPT 继承时,我遇到了死锁错误。每天 5-10 次尝试在此数据库中创建条目时会出现死锁错误。这个数据库是一个网站的日志数据库,所以我会说 90% 的操作都是写入。我用它来记录网站上的活动。
如果我在我的网站的测试实例上施加足够的负载,我可以很容易地让错误发生得非常频繁。我正在尝试从我的 DBA 获取 SQL 跟踪,因为我自己没有能力这样做(即使在测试中)。我知道这将是最有帮助的,当我得到它时,我会把它发回这里,但我的第一个问题是,有什么明显的我做错了吗?我在网上对此进行了研究,试图找出 EF 使用哪种类型的并发,反应不一。有人说它使用 SQL 服务器默认值(读取已提交),而另一些人则说,由于它将查询包装在 TransactionScope 对象中,因此它使用默认的 TransactionScope 隔离级别,即 Serializable。
任何人都可以确定使用什么隔离级别,和/或当您创建作为 TPT 继承树一部分的实体时生成什么 SQL?
谢谢。
编辑 因此,为了响应@usr对我原始帖子的评论的建议,我尝试使用 ReadCommitted 级别将我的保存包装在事务范围中,就像这样
TransactionOptions options = new TransactionOptions();
options.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, options))
{
retVal = AuditEventContext.SaveChanges();
scope.Complete();
}
我仍然遇到同样的死锁错误。我正在从我的 DBA 获取 SQL 跟踪。
为了在这个应用程序周围添加更多上下文,我将添加我的上下文(上面代码中的 AuditEventContext)正在被 MVC 应用程序使用。每个控制器都有自己的 AuditEventContext 实例,因此我在多个线程中有多个上下文来更新同一个数据库。我确定这是导致死锁的原因,我只是不明白为什么,特别是因为我的原始代码模仿了我认为 EF 为基于继承的插入所做的事情。
编辑#2以回答以下问题
还有其他应用程序可以读取这些数据,但是当我知道什么都没有读取时,我可以使这个错误发生。我一次在我的网站上扔了 50 个左右的用户,这几乎是马上发生的。幸运的是,我们的网站上同时没有 50 个用户,但是 :) 我们没有插入很多行。这是一个审计数据库,因此我们在整个站点中编写“审计事件”,即页面请求、接受法律条款、注销等。因此插入的内容非常少。上下文存在于控制器的生命周期中,因此可以使用相同的上下文来插入多个事件,但每个事务都很小。我希望我们不会生成重复的键,因为父表上的键是自动生成的整数标识。管理它是 SQL Server 的工作。父表上有 4 个索引,ALLOW_ROW_LOCKS=ON 和 ALLOW_PAGE_LOCKS=ON。我没有设置这些,我只是保留了 SQL 默认值。索引也有 IGNORE_DUP_KEY=OFF
@RuneG,上下文是由Unity(依赖注入)生成的。我不是直接创建它。我正在调用 Dispose,但是,当拥有上下文的控制器被释放时,我正在调用 Dispose,而不是在调用保存更改之后立即调用,就像我使用 using 语句创建上下文时那样。这会引发问题吗?对多个更新使用相同的上下文?
编辑 #3 - 死锁 SQL
终于得到了一个 SQL 跟踪。下面是一个死锁 SQL 的例子
exec sp_executesql N'insert [dbo].[PFIAccessLog]([AuditEventID], [Screen], [PolicyNumber], [CoverageSequence], [PersonTypeID])
values (@0, @1, @2, @3, @4)
select [PFIAccessLogID]
from [dbo].[PFIAccessLog]
where @@ROWCOUNT > 0 and [AuditEventID] = @0',N'@0 bigint,@1 nvarchar(100),@2 nvarchar(50),@3 nvarchar(50),@4 int',@0=2035631,@1=N'ContractList',@2=N'220001197',@3=N'1',@4=1
和
exec sp_executesql N'insert [dbo].[PFIAccessLog]([AuditEventID], [Screen], [PolicyNumber], [CoverageSequence], [PersonTypeID])
values (@0, @1, @2, @3, @4)
select [PFIAccessLogID]
from [dbo].[PFIAccessLog]
where @@ROWCOUNT > 0 and [AuditEventID] = @0',N'@0 bigint,@1 nvarchar(100),@2 nvarchar(50),@3 nvarchar(50),@4 int',@0=2035630,@1=N'ContractList',@2=N'220001197',@3=N'1',@4=1
PFIAccessLog 是我的 TPT 层次结构中的子表之一。
编辑 4
好的,所以现在我的代码是这样的,但我仍然遇到死锁:
TransactionOptions options = new TransactionOptions();
options.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, options))
{
context.Database.Connection.Open();
context.Database.Connection.EnlistTransaction(Transaction.Current);
retVal = context.SaveChanges();
scope.Complete();
}
我还对代码进行了更改,以在每次编写时创建一个新上下文,而不是在 MVC 控制器的生命周期内共享上下文(可能是多次写入)。
编辑#5
好的,在@RuneG 的建议下,我将隔离级别设置为未提交并解决了问题。但是,我对此有点紧张。如果您查看死锁的示例语句,我正在阅读的部分内容是主键值。如果我可以从另一个事务中读取未提交的数据,并且这些键是由数据库自动生成的,那么当我回读时是否有可能获得一些其他记录 ID?
假设这些 SQL 片段中的每一个都在事务中执行,并且由于它们对同一个表进行 1 次写入和 1 次读取,因此我假设死锁是这样发生的
事务 1 写入它的数据。它获得并释放排他锁。
事务 1 为读取获取共享锁。事务 2 为其插入获取写锁。
Bam,在读取提交下出现死锁,对吧?事务 1 无法继续,因为存在写锁。事务 2 无法继续,因为有一个共享锁来防止事务 2 写入数据 事务 1 可能被视为脏。
所以假设我现在正在阅读未提交的内容。
事务 1 写入它的数据。事务 1 读取它的数据 事务 2 写入它的数据 事务 2 读取它的数据。
其中一个进程可以用独占锁阻塞另一个进程,但不会获得任何共享锁。所以我关心的是主键的生成如何适应所有这些。我假设它是在桌子上有排他锁时生成的,所以我的钥匙没有被“混淆”的风险?我真的不在乎其他进程是否会读取最终回滚的脏数据。钥匙是我关心的。
而且,这是多个线程运行同一种 SQL 语句的结果。线程是我的 MVC 应用程序中的请求。这似乎是一个标准场景。我假设如果我让我的代码线程安全(即一次只有一个线程在代码中),我可以使用读取提交?
抱歉这么啰嗦。我倾向于在解决问题时:)