在 SQL 中使用双重检查锁定死锁

Posted

技术标签:

【中文标题】在 SQL 中使用双重检查锁定死锁【英文标题】:Deadlock using double-checked locking in SQL 【发布时间】:2013-11-28 10:48:46 【问题描述】:

我的 SQL 语句遇到死锁,我想在其中选择一个 ID(如果存在),否则插入然后选择它。正如here 所建议的那样,我正在使用双重检查锁定来防止锁定开销。

显然我这样做是为了支持并发插入,并且我正在运行多个线程。我的 SQL 知识非常低,所以我可能错过了一些关于锁定的基本知识?这是我的程序:

CREATE PROCEDURE InsertAndOrSelectZipCity
@PostalDistrict nvarchar(25),
@CityName nvarchar(34),
@MunicipalityId smallint,
@ZipCode smallint
AS

DECLARE @id AS INT
SELECT @id = ZipCityId FROM ZipCity (NOLOCK) WHERE MunicipalityID=@MunicipalityId AND ZipCode=@ZipCode
IF @id IS NULL
BEGIN
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION
        SELECT @id = ZipCityId FROM ZipCity WHERE MunicipalityID=@MunicipalityId AND ZipCode=@ZipCode
        IF @id IS NULL
        BEGIN
           INSERT INTO ZipCity (PostalDistrict, CityName, MunicipalityId, ZipCode) VALUES (@PostalDistrict, @CityName, @MunicipalityId, @ZipCode)
           SELECT @id = SCOPE_IDENTITY()
        END
    COMMIT TRANSACTION
END
SELECT @id

更新

这可以通过在事务内的 Select 语句上使用适当的锁(XLOCK、ROWLOCK、HOLDLOCK)来解决。

以下是使用 MERGE 语句编写的过程,不需要事务:​​

DECLARE @id as INT

MERGE INTO ZipCity WITH (TABLOCK) AS Target
USING (SELECT @PostalDistrict, @CityName, @MunicipalityId, @ZipCode) AS Source (PostalDistrict, CityName, MunicipalityId, ZipCode)
ON Target.MunicipalityId = Source.MunicipalityId AND Target.ZipCode = Source.ZipCode
WHEN MATCHED THEN
    UPDATE SET @id = Target.ZipCityId
WHEN NOT MATCHED THEN
    INSERT (PostalDistrict, CityName, MunicipalityId, ZipCode) VALUES (@PostalDistrict, @CityName, @MunicipalityId, @ZipCode)
OUTPUT INSERTED.ZipCityId;

【问题讨论】:

【参考方案1】:
    SELECT @id = ZipCityId 
    FROM ZipCity 
    WHERE MunicipalityID=@MunicipalityId 
      AND ZipCode=@ZipCode

在这里,您的选择是获取 S 锁。这可能发生在多个线程上。稍后,插入尝试 X 锁定,这是一个死锁。

立即获取 X 锁:

    SELECT @id = ZipCityId 
    FROM ZipCity WITH (XLOCK, ROWLOCK, HOLDLOCK) ...

在这里,ROWLOCK, HOLDLOCK 不是严格要求,但模式 XLOCK, ROWLOCK, HOLDLOCK 是相当标准的,我尝试在任何地方都遵循它以保持一致性。

顺便说一句,您可能想要切换到 MERGE 语句。我认为它会自动获取 U 锁,因此不需要锁定提示。不过,不确定。无论如何,这将是代码改进和性能改进。

【讨论】:

成功了 - 谢谢!对于 MERGE 建议,如果我的数据是静态的,是否必须使用 UPDATE 语句?例如。每个 ZipCity 行始终相同。 MERGE 可以包含您想要的任何子句组合。你可以只拥有一个 WHEN NOT MATCHED THEN INSERT,没有别的。 Merge 是所有其他 DML 语句的超集。 太棒了。使用 MERGE,之后我是否只需要 SELECT (NOLOCK) ZipCityID 来实现相同的输出和功能? 您使用 OUTPUT 子句:OUTPUT INSERTED.ZipCityId。这会为您发出一个结果集。您还可以将结果流式传输到表变量或临时表中。 MERGE 是解决此类问题的整体解决方案。 SCOPE_IDENTITY 不需要(因为合并可以同时发出多个 ID,单个值可能不够)。 我刚刚测试过您可以使用以下方法提取现有 ID:WHEN MATCHED THEN UPDATE SET @id = target.ID 其中@id 是一些已声明的变量。【参考方案2】:

嗯,很难重现你的场景,但看起来确实很有趣。

尝试使用 ROWLOCK,即做这样的事情

SELECT @id = ZipCityId FROM ZipCity WITH(ROWLOCK) WHERE MunicipalityID=@MunicipalityId AND ZipCode=@ZipCode

看看它是否有帮助(我希望它有帮助)。

另外,您可能想查看这篇文章并查看 如果它与您的场景相关。在我看来是。

http://support.microsoft.com/kb/323630

【讨论】:

仅使用行锁仍然会出现死锁。 usr 使用 (XLOCK, ROWLOCK, HOLDLOCK) 的答案似乎有效。谢谢你的链接。

以上是关于在 SQL 中使用双重检查锁定死锁的主要内容,如果未能解决你的问题,请参考以下文章

为啥在双重检查锁定中使用 Volatile.Write?

双重检查锁定

双重检查锁定与延迟优化

双重检查锁定原理详解

JAVA 双重检查锁定和延迟初始化

双重检查锁定的正确编译器内在函数?