防止合并僵局


9

在我们的一个数据库中,我们有一个表,该表被多个线程密集并发访问。线程确实通过来更新或插入行MERGE。还有一些线程有时会删除行,因此表数据非常不稳定。进行upsert的线程有时会陷入死锁。该问题看起来类似于问题中描述的问题。不过,不同之处在于,在我们的例子中,每个线程确实更新或插入一行

简化的设置如下。该表是堆,上面有两个唯一的非聚集索引

CREATE TABLE [Cache]
(
    [UID] uniqueidentifier NOT NULL CONSTRAINT DF_Cache_UID DEFAULT (newid()),
    [ItemKey] varchar(200) NOT NULL,
    [FileName] nvarchar(255) NOT NULL,
    [Expires] datetime2(2) NOT NULL,
    CONSTRAINT [PK_Cache] PRIMARY KEY NONCLUSTERED ([UID])
)
GO
CREATE UNIQUE INDEX IX_Cache ON [Cache] ([ItemKey]);
GO

典型的查询是

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

MERGE INTO [Cache] WITH (HOLDLOCK) T
USING (
    VALUES (@itemKey, @fileName, dateadd(minute, 10, sysdatetime()))
) S(ItemKey, FileName, Expires)
ON T.ItemKey = S.ItemKey
WHEN MATCHED THEN
    UPDATE
    SET
        T.FileName = S.FileName,
        T.Expires = S.Expires
WHEN NOT MATCHED THEN
    INSERT (ItemKey, FileName, Expires)
    VALUES (S.ItemKey, S.FileName, S.Expires)
OUTPUT deleted.FileName;

即,匹配通过唯一的索引键发生。提示HOLDLOCK是在这里,因为并发(如建议在这里)。

我做了一些小调查,以下是我发现的内容。

在大多数情况下,查询执行计划是

索引搜索执行计划

具有以下锁定模式

索引查找锁定模式

IX锁定对象,然后再进行更细化的锁定。

但是,有时查询执行计划是不同的

表扫描执行计划

(可以通过添加INDEX(0)提示来强制此计划形状),并且其锁定模式为

表扫描锁定模式

通知XIX已经放置在对象上。

由于两个IX是兼容的,但两个X不兼容,因此在并发下发生的事情是

僵局

死锁图

僵局

问题的第一部分出现了。是否XIX符合条件的情况下锁定对象?不是虫子吗?

文档状态:

意向锁之所以称为意向锁,是因为它们是在较低级别的锁之前获取的,因此发出意向将锁置于较低级别的意图

并且

IX表示仅更新部分行而不是全部行的意图

因此,在我看来非常可疑X之后,将物体锁定在物体上IX

首先,我尝试通过尝试添加表锁定提示来防止死锁

MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T

MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T

TABLOCK到位锁定模式变

合并Holdlock Tablock锁定模式

TABLOCKX锁定模式是

合并Holdlock Tablockx锁定模式

由于两个SIX(以及两个X)不兼容,因此可以有效防止死锁,但是不幸的是,还可以防止并发(这是不希望的)。

我的下一个尝试是在加入PAGLOCKROWLOCK使锁具更细化和减少争。两者均无效(X此后仍可观察到物体IX)。

我最后的尝试是通过添加FORCESEEK提示来强制执行具有良好粒度锁定的“良好”执行计划

MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T

而且有效。

问题的第二部分出现了。难道FORCESEEK会被忽略并使用错误的锁定模式吗?(正如我提到的,PAGLOCK并且ROWLOCK似乎被忽略了)。


添加UPDLOCK无效(X对之后仍然可见的对象IX)。

IX_Cache正如预期的那样,使索引聚类。它导致了使用聚集索引查找和粒度锁定的计划。另外,我尝试强制显示聚类索引的“聚集索引扫描”

然而。补充观察。即使在原始设置FORCESEEK(IX_Cache(ItemKey)))中,如果将@itemKey变量声明从varchar(200)更改为nvarchar(200),执行计划也将变为

使用nvarchar的索引查找执行计划

看到使用了搜索,但是在这种情况下,BUT锁定模式再次X在后面显示了对对象的锁定IX

因此,似乎强制寻找并不一定保证粒度锁定(因此也没有死锁)。我不确定具有聚集索引可以保证粒度锁定。还是呢?

我的理解(如果我错了,请纠正我)是锁定在很大程度上是情景的,并且特定的执行计划形状并不意味着特定的锁定模式。

关于仍然打开X后在对象上放置锁定的资格的问题IX。如果符合条件,是否可以做一些事情来防止对象锁定?


有关反馈,访问feedback.azure.com
i-one,

Answers:


9

拼劲IX其次X在对象资格?是不是bug?

看起来有点奇怪,但这是有效的。在进行此操作时IX,其意图很可能是X在较低级别上进行锁定。无话可说,实际上必须采取这种锁。毕竟,可能没有什么可以锁定在较低的级别上了。引擎无法提前知道这一点。另外,可能有一些优化,可以跳过较低级别的锁(此处提供的示例ISS锁)。

更具体地说,对于当前场景,确实可序列化的键范围锁不可用于堆,因此唯一的选择是X对象级别的锁。从这种意义上讲,X如果访问方法是堆扫描,则引擎可能能够及早检测到不可避免地需要锁定,因此避免获取IX锁定。

另一方面,锁定是复杂的,并且出于内部原因有时可能会使用意图锁定,而这些内部原因不一定与采取较低级别的锁定的意图有关。采取IX可能是为某些不起眼的边缘情况提供所需保护的侵入性最小的方法。出于类似的考虑,请参阅IsolationLevel.ReadUncommitted上发布的共享锁

因此,当前的情况对于您的僵局情况是不幸的,并且从原则上讲可以避免,但是不一定与“错误”相同。如果您需要确定的答案,则可以通过常规支持渠道或在Microsoft Connect上报告问题。

难道FORCESEEK会被忽略并使用错误的锁定模式吗?

编号FORCESEEK不是提示,而是指令。如果优化器找不到符合“提示”的计划,则会产生错误。

强制索引是确保可以进行键范围锁定的一种方法。再加上在处理用于更改行的访问方法时自然需要的更新锁,这提供了足够的保证,可以避免方案中的并发问题。

如果表的架构未更改(例如,添加新索引),则提示也足以避免此查询自身死锁。其他查询可能仍会出现循环死锁,这些查询可能会在非聚簇索引之前访问堆(例如对非聚簇索引键的更新)。

...从varchar(200)nvarchar(200)...的变量声明

这破坏了将影响单行的保证,因此引入了一个渴望表假脱机以进行万圣节保护。作为进一步的解决方法,请使用来明确保证MERGE TOP (1) INTO [Cache]...

我的理解是,锁定在很大程度上是一种情况,并且特定的执行计划形状并不意味着特定的锁定方式。

当然,执行计划中还有很多要做的事情。您可以使用例如计划指南来强制确定某个计划形状,但引擎仍可能决定在运行时采取不同的锁定。如果您合并TOP (1)以上元素,则机会很小。

一般说明

看到以这种方式使用堆表有点不寻常。您应该考虑将其转换为集群表的好处,也许可以使用Dan Guzman在注释中建议的索引:

CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);

这可能具有重要的空间重用优势,并为当前的死锁问题提供了一个好的解决方法。

MERGE在高并发环境中也不太常见。有点违反直觉,执行分离INSERTUPDATE声明通常会更有效,例如:

DECLARE
    @itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
    @fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';

BEGIN TRANSACTION;

    DECLARE @expires datetime2(2) = DATEADD(MINUTE, 10, SYSDATETIME());

    UPDATE TOP (1) dbo.Cache WITH (SERIALIZABLE, UPDLOCK)
    SET [FileName] = @fileName,
        Expires = @expires
    OUTPUT Deleted.[FileName]
    WHERE
        ItemKey = @itemKey;

    IF @@ROWCOUNT = 0
        INSERT dbo.Cache
            (ItemKey, [FileName], Expires)
        VALUES
            (@itemKey, @fileName, @expires);

COMMIT TRANSACTION;

注意如何不再需要RID查找:

执行计划

如果可以保证ItemKey(如问题中)存在唯一索引TOP (1),则UPDATE可以删除中的冗余,从而给出更简单的计划:

简化的更新

无论哪种情况INSERTUPDATE计划都符合琐碎的计划。MERGE始终需要基于成本的全面优化。

有关使用的正确模式,请参阅相关的Q&A SQL Server 2014并发输入问题,以及有关的更多信息MERGE

死锁无法始终避免。通过仔细的编码和设计,可以将它们减少到最低限度,但是应用程序应始终准备好优雅地处理奇数死锁(例如,重新检查条件然后重试)。

如果您完全控制访问有问题的对象的过程,则还可以考虑使用应用程序锁来序列化对单个元素的访问,如SQL Server并行插入和删除中所述

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.