Cod*_*awk 57 sql-server-2008 sql-server deadlock
最近我们的一个 ASP.NET 应用程序显示了一个数据库死锁错误,我被要求检查并修复该错误。我设法找到死锁的原因是一个存储过程,它严格更新游标内的表。
这是我第一次看到这个错误,不知道如何有效地跟踪和修复它。我尝试了我知道的所有可能的方法,最后发现正在更新的表没有主键!幸运的是,这是一个身份栏。
后来我发现为部署编写数据库脚本的开发人员搞砸了。我添加了一个主键,问题就解决了。
我感到很高兴并回到我的项目中,并做了一些研究以找出导致僵局的原因......
显然,这是导致死锁的循环等待条件。没有主键的更新显然比有主键需要更长的时间。
我知道这不是一个明确定义的结论,这就是我在这里发布的原因......
小智 40
跟踪死锁是两者中更容易的一个:
默认情况下,错误日志中不会写入死锁。您可以使用跟踪标志 1204 和 3605 导致 SQL 将死锁写入错误日志。
将死锁信息写入 SQL Server 错误日志:DBCC TRACEON(-1, 1204, 3605)
关闭它:DBCC TRACEOFF(-1, 1204, 3605)
有关跟踪标志 1204 和打开时将获得的输出的讨论,请参阅“对死锁进行故障排除”。 https://msdn.microsoft.com/en-us/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
...
Run Code Online (Sandbox Code Playgroud)
代码块 B(伪代码)
Lock Shared Resource Z
Lock Resource B
Lock Resource A
Unlock Shared Resource Z
...
Run Code Online (Sandbox Code Playgroud)
完成后不要忘记解锁 A 和 B
这将防止代码块 A 和代码块 B 之间的死锁
从数据库的角度来看,我不确定如何防止这种情况,因为锁是由数据库本身处理的,即更新数据时的行/表锁。我看到最多问题发生的地方是您在光标内看到的地方。游标是出了名的低效,尽可能避免使用它们。
Mar*_*ian 24
我最喜欢阅读和了解死锁的文章有: 简单谈话 - 追踪死锁 和 SQL Server Central - 使用 Profiler 解决死锁。他们会为您提供有关如何处理糟糕情况的样本和建议。
简而言之,为了解决当前的问题,我会缩短所涉及的事务,从中取出不需要的部分,注意对象的使用顺序,看看实际需要什么隔离级别,而不是不需要读数据...
但是最好阅读这些文章,它们的建议会更好。
Joe*_*Joe 16
有时死锁可以通过添加索引来解决,因为它允许数据库锁定单个记录而不是整个表,因此您可以减少争用和事情卡住的可能性。
例如,在InnoDB 中:
如果您没有适合您的语句的索引,并且 MySQL 必须扫描整个表来处理该语句,则该表的每一行都会被锁定,从而阻止其他用户对该表的所有插入。创建好的索引很重要,这样您的查询就不会不必要地扫描很多行。
另一个常见的解决方案是在不需要时关闭事务一致性,或者以其他方式更改您的隔离级别,例如,计算统计信息的长时间运行的作业......一个接近的答案通常就足够了,您不需要精确的数字,因为他们正在从你之下改变。如果需要 30 分钟才能完成,您不希望它停止这些表上的所有其他事务。
...
至于跟踪它们,这取决于您使用的数据库软件。
只是为了开发光标的东西。这确实很糟糕。它锁定整个表,然后一一处理行。
最好使用 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
Run Code Online (Sandbox Code Playgroud)
如果您的 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
Run Code Online (Sandbox Code Playgroud)
小智 6
缺少主键不是问题。至少就其本身而言。首先,您不需要主索引即可。其次,即使您正在执行表扫描(如果您的特定查询不使用索引,则必须发生这种情况,表锁本身不会导致死锁。写进程将等待读取,而读取进程将等待写入,当然读取根本不必等待对方。
添加到其他答案中,事务隔离级别很重要,因为可重复读取和序列化是导致“读取”锁一直保持到事务结束的原因。锁定资源不会导致死锁。保持锁定确实如此。写操作始终保持其资源锁定,直到事务结束。
我最喜欢的锁定预防策略是使用“快照”功能。Read Committed Snapshot 功能意味着读取不使用锁!如果您需要比“已提交读”更多的控制,则可以使用“快照隔离级别”功能。这允许序列化(此处使用 MS 术语)事务发生,而不会阻止其他玩家。
最后,可以通过使用更新锁来防止一类死锁。如果您读取并保持读取(HOLD,或使用可重复读取),并且另一个进程也这样做,那么两者都尝试更新相同的记录,您将出现死锁。但是如果两个进程都请求更新锁,第二个进程将等待第一个进程,同时允许其他进程使用共享锁读取数据,直到实际写入数据。如果其中一个进程仍然请求共享 HOLD 锁,这当然不起作用。