【问题标题】:Deadlock with single stored procedure and multiple threads单存储过程多线程死锁
【发布时间】:2014-11-24 17:49:06
【问题描述】:

我正在多线程环境中对 SQL 事务进行一些测试。我试图通过在一个循环中从并行运行的 2 个线程执行单个存储过程来生成死锁。我的两个线程在启动时都使用相同的方法,它连续执行一个存储过程:

using (TestDataContext db = new TestDataContext())
{
    while (true)
    {
        db.DeadLocking();
    }
}

有人可以举一个“死锁”存储过程的例子,在这种情况下会可靠地产生死锁。它必须使用事务(单个或多个)。我已经研究了很多,并看到了许多关于如何在 sql 中生成死锁的示例,但是,它们都没有在我的代码中起作用。请帮忙。

更新:按照 Marc 的建议,我尝试了这个 sproc,但无济于事:

CREATE PROCEDURE [dbo].[DeadLocking]
AS
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
SET NOCOUNT ON
        BEGIN TRANSACTION
            DECLARE @val varchar(1)
            SELECT @val = Record FROM Test.dbo.Records WHERE RecordId = 1
            UPDATE Test.dbo.Records SET Record = @val WHERE RecordId = 1
        COMMIT TRANSACTION
END

从两个线程并行运行它应该将这些线程锁定在彼此上。我做错了什么?

更新:上述过程确实会导致死锁,但是,它至少需要 3 个线程而不是 2 个(不知道为什么,可能需要 2 个但也需要永远)。有趣的是,这也会导致死锁:

CREATE PROCEDURE [dbo].[DeadLocking]
AS
BEGIN
    SET NOCOUNT ON
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    UPDATE Test.dbo.Records SET Record = 1 WHERE RecordId = 1
END

我猜这是因为存储过程本身在幕后实现了某种事务逻辑。如果有人有更多关于为什么会发生的信息,请分享。 请注意,死锁仅发生在 UPDATE 上,不会发生在 SELECT 上。这发生在 SERIALIZABLE 和 REPEATABLE READ 隔离级别上。

【问题讨论】:

  • 经典的死锁方法:使用“可序列化”隔离级别,让 spid A 对某些数据进行读锁(没有updlock);让 spid B 对 相同的数据 进行读取锁定;现在 spid A 如何对相同的数据进行写锁定(尝试更改列),并让 spid B 尝试执行相同操作。他们现在都陷入了僵局。
  • @Marc,我认为简单的 SELECT 不会在一行上获得读锁,对吗?
  • 在可序列化的隔离级别;是的:它会的。在大多数其他隔离级别:否
  • 重新编辑:这是一个 线程竞赛 场景 - 除非您从外部手动编排它,否则您完全有可能永远看不到这里的冲突,除非你迭代它成千上万次。您可以做的一件事是在SELECTUPDATE 之间添加WAITFOR 延迟(可能是5 秒),这样两个SPID 在尝试读取之前就有更高的机会拥有读锁进行更新。
  • 那里不需要被动攻击姿态

标签: c# sql sql-server multithreading stored-procedures


【解决方案1】:

考虑在选择中请求更新锁。 WITH (UPDLOCK)。

这确保 SELECT 已经更新了记录。

【讨论】:

  • “我正在尝试产生死锁” - 就实现该目标而言,这似乎与有用的完全相反
【解决方案2】:

要创建死锁,您肯定需要两个不同的过程,或者至少是执行分支,它们以不同的顺序获取锁。

事实上,确保获取锁的严格顺序是防止死锁的众所周知的方法。

因此,您将需要 两个 不同的锁 A 和 B,例如在两个不同的表上。然后一个线程会尝试锁定 A then B,而另一个线程会尝试锁定 B then A。只有这样才有机会造成死锁。

如果您想增加在运行时实际发生死锁的概率,则需要一些延迟,例如:

lock A
delay5Seconds
lock B
delay5Seconds
unlock B
unlock A

lock B
delay5Seconds
lock A
delay5Seconds
unlock A
unlock B

这允许其他线程在正确的时间点发生死锁。

为了使每次都确定性地生成死锁,必须创建一种机制来同步两个线程执行,例如

lock A
wait for thread #2 to lock B
lock B
...

lock B
wait for thread #1 to lock A
lock A
...

编辑:

发现这个似乎相关的 SO 问题:Confused about UPDLOCK, HOLDLOCK

据此推断,您可能会这样尝试:

BEGIN TRANSACTION
SELECT * FROM Test.dbo.Records WHERE RecordId = 1 WITH (UPDLOCK, HOLDLOCK)
WAITFOR DELAY '00:00:10'
SELECT * FROM Test.dbo.Records WHERE RecordId = 999999 WITH (UPDLOCK, HOLDLOCK)
WAITFOR DELAY '00:00:10'
COMMIT TRANSACTION

BEGIN TRANSACTION
SELECT * FROM Test.dbo.Records WHERE RecordId = 999999 WITH (UPDLOCK, HOLDLOCK)
WAITFOR DELAY '00:00:10'
SELECT * FROM Test.dbo.Records WHERE RecordId = 1 WITH (UPDLOCK, HOLDLOCK)
WAITFOR DELAY '00:00:10'
COMMIT TRANSACTION

原则上,这应该可以解决所有事务隔离级别的问题。 (您在两个线程中都使用了 explicit 锁,因此 DBMS 忽略它会很大胆。) 但是,我完全不确定来自同一个表的两个连续选择是否实际上会获得两个单独的锁(每个受影响的记录上一个),或者 DBMS 是否可能只是决定锁定整个表,或者至少一个它的范围,因此只有一个锁被有效地持有。因此,为了确保您实际上在不同的时间点获得了两个锁,您实际上可能希望将两个选择分散到两个不同的表中。

【讨论】:

  • 如果你使用正确的锁,我认为应该这样做。 TomTom 关于WITH (UPDLOCK) 用于显式write 锁定的建议听起来很有希望。另一方面,如果您想通过组合读取和写入锁来创建死锁情况,则结果可能取决于隔离级别。
  • 是的,伙计,这会产生死锁,但是,它使用 UPDLOCK 明确建立所需的锁并使用 2 个相互锁定的不同事务,这与我的问题有点不同。但感谢您提供更多信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-16
  • 1970-01-01
  • 1970-01-01
  • 2011-10-22
相关资源
最近更新 更多