死锁的主要原因是什么,可以防止死锁吗?


55

最近,我们的一个ASP.NET应用程序显示了一个数据库死锁错误,要求我检查并修复该错误。我设法找到导致死锁的原因是存储过程正在严格更新游标中的表。

这是我第一次看到此错误,并且不知道如何有效地跟踪和修复它。我尝试了所有可能的方法,最后发现正在更新的表没有主键!幸运的是,这是一个身份专栏。

后来,我发现为脚本编写部署脚本的开发人员陷入困境。我添加了一个主键,问题就解决了。

我感到很高兴,回到了我的项目,并做了一些研究以找出造成这种僵局的原因...

显然,导致死锁的是循环等待条件。没有主键的更新显然要比使用主键的更新花费更长的时间。

我知道这不是一个明确的结论,这就是为什么我在这里发布...

  • 缺少主键是问题所在吗?
  • 除了互斥,保留和等待,没有抢占和循环等待之外,还有其他导致僵局的条件吗?
  • 如何防止和跟踪死锁?

2
我见过的IME多数(全部?)死锁是由于循环等待(主要是由于过度使用触发器)引起的。
2011年

循环性是僵局的必要条件之一。如果所有会话都以相同顺序获取锁,则可以避免任何死锁。
Peter G.

Answers:


38

跟踪死锁是两者中较容易的:

默认情况下,死锁不会写入错误日志中。您可以使SQL使用跟踪标志1204和3605将死锁写入错误日志。

将死锁信息写入SQL Server错误日志:DBCC TRACEON(-1、1204、3605)

将其关闭:DBCC TRACEOFF(-1,1204,3605)

有关跟踪标志1204以及打开该标志时所获得的输出的讨论,请参见“对死锁进行故障排除”。 https://msdn.microsoft.com/zh-CN/library/ms178104.aspx

预防更加困难,从本质上讲,您必须注意以下事项:

代码块1依次锁定资源A,然后锁定资源B。

代码块2依次锁定资源B,然后锁定资源A。

这是可能发生死锁的经典情况,如果两个资源的锁定都不是原子的,则代码块1可以锁定A并被抢占,然后代码块2会在A恢复处理时间之前锁定B。现在您陷入僵局。

为了防止这种情况,您可以执行以下操作

代码块A(伪代码)

Lock Shared Resource Z
    Lock Resource A
    Lock Resource B
Unlock Shared Resource Z
...

代码块B(伪代码)

Lock Shared Resource Z
    Lock Resource B
    Lock Resource A
Unlock Shared Resource Z
...

完成A和B后,不要忘记解锁

这样可以防止代码块A和代码块B之间的死锁

从数据库的角度来看,我不确定如何避免这种情况,因为锁由数据库本身处理,即更新数据时的行/表锁。我看到最多的问题是在光标内看到问题的位置。众所周知,游标的效率很低,请尽可能避免使用游标。


您是否要在代码块B中将资源A锁定在资源B之前?如所写,这将导致死锁..正如您自己在之前的评论中提到的。即使在开始时需要虚拟查询来确保锁定顺序,也要尽可能始终以相同的顺序锁定资源。
Gerard ONeill

23

我最喜欢阅读和了解死锁的文章包括: 简单对话-追踪死锁SQL Server Central-使用探查器解决死锁。他们将为您提供有关如何处理不良情况的样本和建议。

简而言之,为了解决当前的问题,我将使所涉及的事务更短,从其中删除不需要的部分,照顾对象的使用顺序,查看实际需要的隔离级别,而不是不需要读取数据...

但是,最好阅读文章,它们会提供更好的建议。


16

有时可以通过添加索引来解决死锁,因为它允许数据库锁定单个记录而不是整个表,因此您可以减少争用和事物阻塞的可能性。

例如,在InnoDB中

如果没有适合您的语句的索引,并且MySQL必须扫描整个表以处理该语句,则表的每一行都将被锁定,这又会阻止其他用户对表的所有插入。创建良好的索引很重要,这样您的查询就不必不必要地扫描很多行。

另一个常见的解决方案是在不需要事务一致性时关闭它,或者以其他方式更改隔离级别,例如,一项长期的工作来计算统计信息……通常,一个明确的答案就足够了,您不需要精确的数字,因为他们从你下面变了。而且,如果需要30分钟才能完成,则您不希望它停止这些表上的所有其他事务。

...

至于跟踪它们,取决于您使用的数据库软件。


降低投票率时通常会提供评论...这是一个有效的答案,将select语句升级为表锁并且永久使用肯定会导致死锁。
BlackICE 2011年

1
如果索引不是群集的,则MS SQLServer也可以提供意外的锁定行为。它会默默地忽略您使用行级锁定的方向,并将执行页级锁定。然后,您可以在页面上等待死锁。
2013年

7

只是为了开发光标的东西。确实是很糟糕。它锁定整个表,然后一一处理行。

最好使用while循环以游标的形式遍历行

在while循环中,将对循环中的每一行执行一次选择,并且锁定一次仅发生在一行上。表中的其余数据可自由查询,从而减少了发生死锁的机会。

再加上它更快。让您想知道为什么仍然存在游标。

这是这种结构的示例:

DECLARE @LastID INT = (SELECT MAX(ID) FROM Tbl)
DECLARE @ID     INT = (SELECT MIN(ID) FROM Tbl)
WHILE @ID <= @LastID
    BEGIN
    IF EXISTS (SELECT * FROM Tbl WHERE ID = @ID)
        BEGIN
        -- Do something to this row of the table
        END

    SET @ID += 1  -- Don't forget this part!
    END

如果您的ID字段稀疏,则可能需要提取一个单独的ID列表并进行迭代:

DECLARE @IDs TABLE
    (
    Seq INT NOT NULL IDENTITY PRIMARY KEY,
    ID  INT NOT NULL
    )
INSERT INTO @IDs (ID)
    SELECT ID
    FROM Tbl
    WHERE 1=1  -- Criteria here

DECLARE @Rec     INT = 1
DECLARE @NumRecs INT = (SELECT MAX(Seq) FROM @IDs)
DECLARE @ID      INT
WHILE @Rec <= @NumRecs
    BEGIN
    SET @ID = (SELECT ID FROM @IDs WHERE Seq = @Seq)

    -- Do something to this row of the table

    SET @Seq += 1  -- Don't forget this part!
    END

6

缺少主键不是问题。至少是本身。首先,您不需要主数据库即可拥有索引。其次,即使您正在执行表扫描(如果您的特定查询未使用索引也必须进行表扫描,表锁本身也不会导致死锁。写入过程将等待读取,读取过程将等待读取等待写操作,当然,读取完全不必等待对方。

除其他答案外,事务隔离级别也很重要,因为可重复的读取和序列化是导致“读取”锁一直保持到事务结束的原因。锁定资源不会导致死锁。保持锁定状态。写操作始终将其资源锁定到事务结束为止。

我最喜欢的锁定预防策略是使用“快照”功能。读取已提交快照功能意味着读取不使用锁!而且,如果您需要比“读取已提交”更多的控制权,则可以使用“快照隔离级别”功能。这允许在不阻止其他玩家的情况下进行序列化的交易(在此使用MS术语)。

最后,可以通过使用更新锁来防止一类死锁。如果您读取并保持读取(HOLD,或使用重复读取),并且另一个进程执行相同的操作,则两者都尝试更新相同的记录,则将出现死锁。但是,如果两者都请求更新锁,则第二个进程将等待第一个进程,同时允许其他进程使用共享锁读取数据,直到实际写入数据为止。如果其中一个进程仍请求共享的HOLD锁,那么这当然是行不通的。


-2

尽管SQL Server中的游标速度很慢,但您可以通过将游标的源数据拉入Temp表并在其上运行游标来避免游标死锁。这样可以使光标免于锁定实际数据表,并且您获得的唯一锁定仅用于在光标内部执行的更新或插入,这些锁定仅在插入/更新期间保持,而不在光标持续期间保持。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.