试图防止这种死锁发生在我在 SQL Server 上的 .NET 代码中
Posted
技术标签:
【中文标题】试图防止这种死锁发生在我在 SQL Server 上的 .NET 代码中【英文标题】:Trying to prevent this Deadlock from happening in my .NET code on SQL Server 【发布时间】:2017-02-15 23:37:19 【问题描述】:在我对一些 C# 代码的集成测试期间发生了死锁,该代码针对 SQL Server 测试我的 SQL 查询。
软件:SQL Server 2012 数据库:READ COMMITTED ISOLATION
设置为 ON
我有两个相互竞争的测试,因为这是并行发生的。
测试 1(伪 C# 代码)
using (start a new transaction)
foreach Person in the People-Collection
Insert this Person into the DB and grab the ID of this inserted person.
=> INSERT INTO [dbo].[People] (...) VALUES (...);
SELECT CAST(SCOPE_IDENTITY() AS INT);
Now, count how many people are in the DB right now (because there might have been more, from before we inserted these temp people).
=> SELECT COUNT(PersonId) FROM [dbo].[People]
// end transaction (which rolls back because of no explicit transaction.commit)
测试 2
using (start a new transaction)
Update a person
=> UPDATE [dbo].[People]
SET .....
WHERE PersonId = @personId
// end transaction which auto rolls back.
所以我猜正在发生以下情况:
Test1 运行 INSERT/SELECT Test2 运行 UPDATE(等待 test1 完成) Test1 完成,但现在运行 SELECT COUNT(..)由于某种原因,test1 正在等待 test2 完成更新,但 test2 无法更新(出于我无法想象的原因)。
我原以为会发生以下情况应该:
Test1 开始运行(并在 TRANSACTION 中)。 Test2 开始运行 .. 并注意到有一个事务正在运行,因此它要么等待该事务完成 要么 可以发现它的更新与 无关 test1 的事务,因此它会更新。 Test1 最终完成。显然,我不明白发生了什么:(
索引 死锁图列出了一个特定的索引作为悲伤的原因。这是索引:
ALTER TABLE [dbo].[People] ADD CONSTRAINT [IX_People_Name] UNIQUE NONCLUSTERED
(
[Name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
有人可以就我如何解决这个问题提出一些建议吗?
注意:This gist 包含完整的死锁图作为参考。
编辑 1:添加了 using
语句代码,以便更好地理解逻辑/流程。
编辑 2:添加索引架构/代码
【问题讨论】:
>>结束事务(因为没有明确的transaction.commit而回滚)你在做一个明确的回滚吗? 没有。该代码位于 .netusing
范围内,因此它会自动回滚,因为没有明确的 commit()
;
这张表上有哪些索引? SELECT COUNT(PersonId) FROM [dbo].[People]
的查询计划是什么样的?
不要使用SELECT CAST(SCOPE_IDENTITY()
。使用Output Inserted...
您可以将with nolock
提示添加到您的选择中
【参考方案1】:
您在名称列上有一个索引,因此更新表现为删除+插入以用于锁定/IO 目的。
死锁图显示 Test1 等待获取 RangeS-S
锁以计算所有行,同时在插入的行上持有 RangeX-X
锁。它需要整个表的共享锁来计算行数和插入行的排他锁(不知道为什么它使用范围锁)。
与此同时,Test2 正在等待获取 RangeI-N
锁以插入新名称行,同时在已删除的旧名称行上持有 X
锁。
我会建议以下之一:
在主键(PersonID?
) 字段上创建一个窄唯一索引 - 该索引不需要锁定更新,可用于快速计数(*),维护成本非常低。它还提高了外键完整性检查的并发性和性能(要使 FK-s 引用 [People]
表生效,必须重新创建)。
更改计数查询的锁定(如果业务要求允许) - 例如使用WITH(READCOMMITTED)
表提示或在提交插入后执行。
在插入之前运行计数查询,并按插入的行计数递增(这可能不利于并发,因为它持有整个表的共享锁)
【讨论】:
嗨@pent - 感谢您的回答。有没有机会请您详细说明您的答案。我现在真的不明白这意味着什么。这些锁好不好?他们应该这样做吗?对于如何改进以消除这些问题,还有其他建议吗? 我正在尝试了解您的回答的技术要点......所以当我解决这个问题时,我将发布奇怪的问题。首先 - 当我没有针对该特定字段执行任何WHERE
子句时,为什么代码会使用 IX_People_Name
作为索引?
索引需要为新的/更改的行更新。计数查询最有可能使用索引,因为它是表上最小(最窄)的索引 - 它是枚举所有行的最快方法。
为什么计数使用IX_People_Name
索引而不是主键?主键不是该表上最小(最窄)的索引吗?
主键很可能是一个聚集索引 - 它包含所有列,即整个表。以上是关于试图防止这种死锁发生在我在 SQL Server 上的 .NET 代码中的主要内容,如果未能解决你的问题,请参考以下文章