试图防止这种死锁发生在我在 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而回滚)你在做一个明确的回滚吗? 没有。该代码位于 .net using 范围内,因此它会自动回滚,因为没有明确的 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 代码中的主要内容,如果未能解决你的问题,请参考以下文章

sql server 2005 死锁在生产中超时,而不是在测试环境中:为啥?

(4.7)怎么捕获和记录SQL Server中发生的死锁?

UPDATE 上的 SQL Server 死锁

减少SQL Server数据库死锁的技巧

sql server 死锁自动释放

使用SQL SERVER PROFILER 捕获和分析思索