为啥会出现死锁?

Posted

技术标签:

【中文标题】为啥会出现死锁?【英文标题】:Why deadlock occurs?为什么会出现死锁? 【发布时间】:2011-08-08 19:45:26 【问题描述】:

我使用一个由两个简单查询组成的小事务:选择和更新:

SELECT * FROM XYZ WHERE ABC = DEF

UPDATE XYZ SET ABC = 123
WHERE ABC = DEF

当事务由两个线程启动时,经常发生这种情况,并且取决于隔离级别发生死锁(RepeatableRead,Serialization)。两个事务都尝试读取和更新完全相同的行。 我想知道为什么会这样。导致死锁的查询顺序是什么?我已经阅读了一些关于锁(共享、独占)以及每个隔离级别的锁持续多长时间,但我仍然不完全理解......

我什至准备了一个简单的测试,它总是会导致死锁。我查看了 SSMS 和 SQL Server Profiler 中的测试结果。我开始第一个查询,然后立即开始第二个。

第一个查询:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT ...
WAITFOR DELAY '00:00:04'
UPDATE ...
COMMIT

第二次查询:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT ...
UPDATE ...
COMMIT

现在我无法向您显示详细的日志,但它看起来或多或少像这样(我很可能在某处错过了 Lock:deadlock 等):

(1) SQL:BatchStarting: First query
(2) SQL:BatchStarting: Second query
(3) Lock:timeout for second query
(4) Lock:timeout for first query
(5) Deadlock graph

如果我对锁有很好的理解,在 (1) 中,第一个查询需要一个共享锁(执行 SELECT),然后进入睡眠状态并保持共享锁直到事务结束。在(2)中,第二个查询也使用共享锁(SELECT)但不能使用排他锁(UPDATE),而同一行上有共享锁,这会导致Lock:timeout。但我无法解释为什么会发生第二次查询超时。可能我不太了解整个过程。谁能给个解释?

我没有注意到使用 ReadCommitted 的死锁,但我担心它们可能会发生。 您推荐什么解决方案?

【问题讨论】:

不是你问的,但你为什么要先选择,然后更新?换句话说,为什么不使用只包含更新语句的事务呢? 我简化了我的问题。首先我检查最后修改日期,然后根据值做一些事情,然后更改日期。在这个事务中,有更多的查询,但上面的这些会导致死锁问题。我试图完全理解是什么原因,因为我以前从未听说过在互联网上发现的锁和信息不足以满足我:) 在我的情况下,脏读不是问题,所以我决定选择未提交的读取。我使用 realizable 只是为了看看会发生什么,一种实验 :) 应该是serializable而不是realizable :)我忘了说我用的是c#,不过没关系。 【参考方案1】:

当两个或多个任务由于每个任务对其他任务试图锁定的资源锁定而永久相互阻塞时,就会发生死锁

http://msdn.microsoft.com/en-us/library/ms177433.aspx

【讨论】:

如果隔离级别是 SERIALIZABLE 怎么会发生这种情况?! 查看以下链接以获得更好的交易级别描述,Muhammad brentozar.com/isolation-levels-sql-server【参考方案2】:

“但我无法解释为什么会发生第二次查询超时。”

因为第一个查询持有共享锁。然后第一个查询中的更新也尝试获取排他锁,这让他睡着了。所以第一个和第二个查询都在休眠等待另一个唤醒 - 这是一个导致超时的死锁:-)

mysql 中效果更好 - 立即检测到死锁并回滚其中一个事务(您无需等待超时 :-))。

此外,在 mysql 中,您可以执行以下操作来防止死锁

select ... for update

这将在事务开始时放置一个写锁(即排他锁),这样您就可以避免死锁情况!也许你可以在你的数据库引擎中做类似的事情。

【讨论】:

我的意思是第一次查询超时(但这是第二次超时)。我的错。在大卫的帖子之后,一切都清楚了:) 但是感谢您的回答。 两者的原因相同——原因是死锁。尝试找出您是否可以进行我在回答中提到的预防。【参考方案3】:

对于 MSSQL,有一种机制可以防止死锁。您在这里需要的是 WITH NOLOCK 提示。

在 99.99% 的 SELECT 语句的情况下,它是可用的,不需要将 SELECT 与 UPDATE 捆绑在一起。也不需要将 SELECT 放入事务中。唯一的例外是不允许脏读。

将您的查询更改为此表单将解决您的所有问题:

SELECT ...
FROM yourtable WITH (NOLOCK)
WHERE ...

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
UPDATE ...
COMMIT

【讨论】:

使用WITH (NOLOCK)更改所有查询?【参考方案4】:

我上次处理这个问题已经很长时间了,但我相信 select 语句会创建一个读锁,它只会阻止数据被更改——因此多个查询可以持有并共享一个读锁在相同的数据上。 shared-read-lock 是为了读取一致性,也就是说,如果你在事务中多次读取同一行,那么读取一致性应该意味着你应该总是得到相同的结果。

更新语句需要排他锁,因此更新语句必须等待读锁被释放。

两个事务都不会释放锁,因此事务失败。

不同的数据库实现有不同的策略来处理这个问题,Sybase 和 MS-SQL 服务器使用超时锁升级(从读到写锁升级)——我相信 Oracle(在某些时候)通过使用回滚日志实现读取一致性,其中 MySQL 有不同的策略。

【讨论】:

在处理 SQL Server 中的死锁和锁升级时,可以通过使用隔离级别来解决一些锁争用问题。但是,这可能会引入它自己的一组数据不准确问题。 Kendra little 在这里画了一个小卡通来比较:littlekendra.com/2011/02/08/isoposterMSDN 隔离级别文章在这里:msdn.microsoft.com/en-us/library/aa213034(v=sql.80).aspx

以上是关于为啥会出现死锁?的主要内容,如果未能解决你的问题,请参考以下文章

为啥与多个 Popen 子进程一起使用时会出现通信死锁?

为啥在使用等待组和通道时会出现死锁?

为啥这个事务会产生死锁?

MySQL锁等待与死锁问题分析

MySQL锁等待与死锁问题分析

MySQL锁等待与死锁问题分析