i-o*_*one 15 sql-server deadlock execution-plan merge
在我们的一个数据库中,我们有一个由多个线程密集并发访问的表。线程确实通过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
Run Code Online (Sandbox Code Playgroud)
典型的查询是
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;
Run Code Online (Sandbox Code Playgroud)
即,匹配通过唯一索引键发生。提示HOLDLOCK
在这里,因为并发性(如建议here)。
我做了一些小调查,以下是我发现的。
在大多数情况下,查询执行计划是
使用以下锁定模式
即IX
锁定对象,然后是更细粒度的锁定。
然而,有时查询执行计划是不同的
(这个计划形状可以通过添加INDEX(0)
提示来强制),其锁定模式为
通知X
锁定已放置在对象上IX
。
由于两个IX
兼容,但两个X
不兼容,并发下发生的事情是
僵局!
这里出现了问题的第一部分。符合条件X
后是否锁定对象IX
?不是bug吗?
文档说明:
意向锁之所以被称为意向锁,是因为它们是在较低级别的锁之前获取的,因此表示意图将锁放置在较低级别。
并且还
IX 表示只更新部分行而不是所有行的意图
所以,在我看来非常可疑X
之后锁定对象IX
。
首先,我尝试通过添加表锁定提示来防止死锁
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T
Run Code Online (Sandbox Code Playgroud)
和
MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T
Run Code Online (Sandbox Code Playgroud)
与TABLOCK
到位锁定模式变
和TABLOCKX
锁定模式是
由于两个SIX
(以及两个X
)不兼容,因此可以有效地防止死锁,但不幸的是,也防止了并发(这是不希望的)。
我的下一个尝试是添加PAGLOCK
和ROWLOCK
使锁更细化并减少争用。两者都没有影响(X
在 之后仍然立即观察到对象IX
)。
我的最后一次尝试是通过添加FORCESEEK
提示来强制使用具有良好粒度锁定的“良好”执行计划形状
MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
Run Code Online (Sandbox Code Playgroud)
它奏效了。
这里出现了问题的第二部分。会不会发生这种情况FORCESEEK
会被忽略并使用错误的锁定模式?(正如我所提到的,PAGLOCK
和ROWLOCK
被看似忽略)。
添加UPDLOCK
没有效果(X
在对象之后仍然可以观察到IX
)。
IX_Cache
正如预期的那样,使索引聚集工作。它导致了聚集索引搜索和粒度锁定的计划。此外,我还尝试强制显示粒度锁定的聚集索引扫描。
然而。额外的观察。在原始设置中,即使FORCESEEK(IX_Cache(ItemKey)))
在适当的位置,如果将@itemKey
变量声明从varchar(200)更改为nvarchar(200),则执行计划变为
看到使用了seek,但是在这种情况下的锁定模式再次显示X
锁定后放置在对象上IX
。
因此,似乎强制查找不一定保证粒度锁(因此不存在死锁)。我不相信聚集索引能保证粒度锁定。或者是吗?
我的理解(如果我错了请纠正我)是锁定在很大程度上是情境性的,并且某些执行计划形状并不意味着某些锁定模式。
关于仍然打开X
后锁定对象的资格问题IX
。如果它符合条件,是否可以采取一些措施来防止对象锁定?
Pau*_*ite 13
在对象上放置
IX
后跟是否X
符合条件?是不是bug?
看起来有点奇怪,但它是有效的。在IX
获取时,意图很可能是X
在较低级别获取锁。没有什么可以说这种锁实际上必须被采取。毕竟,在较低级别可能没有任何东西可以锁定;引擎无法提前知道。此外,可能会有一些优化,以便可以跳过较低级别的锁(可以在此处查看IS
和S
锁的示例)。
更具体地说,对于当前场景,确实可序列化的键范围锁不可用于堆,因此唯一的选择是X
对象级别的锁。从这个意义上说,X
如果访问方法是堆扫描,引擎可能能够及早检测到锁将不可避免地需要,因此避免获取IX
锁。
另一方面,锁定是复杂的,有时可能出于内部原因而采用意图锁,而与采用较低级别锁的意图不一定相关。采取IX
可能是为一些模糊的边缘情况提供所需保护的侵入性最小的方式。对于类似的考虑,请参阅在 IsolationLevel.ReadUncommitted 上发布的共享锁。
因此,当前的情况对于您的死锁场景来说是不幸的,并且原则上可能是可以避免的,但这不一定与“错误”相同。如果您需要明确的答案,您可以通过您的正常支持渠道或 Microsoft Connect 报告问题。
会不会发生这种情况
FORCESEEK
会被忽略并使用错误的锁定模式?
号FORCESEEK
不是一个提示,而是一个指令。如果优化器找不到符合“提示”的计划,它将产生错误。
强制索引是确保可以采取键范围锁定的一种方式。连同在处理要更改的行的访问方法时自然采用的更新锁,这提供了足够的保证来避免您的场景中的并发问题。
如果表的模式没有改变(例如添加新索引),提示也足以避免此查询与自身发生死锁。仍然有可能与其他可能在非聚集索引之前访问堆的查询(例如对非聚集索引的键的更新)发生循环死锁。
...变量声明从
varchar(200)
到nvarchar(200)
...
这打破了单行会受到影响的保证,因此引入了 Eager Table Spool 以用于万圣节保护。作为对此的进一步解决方法,请使用MERGE TOP (1) INTO [Cache]...
.
我的理解 [...] 是锁定在很大程度上是情境性的,某些执行计划形状并不意味着某些锁定模式。
在执行计划中可以看到的当然还有更多。您可以使用计划指南等强制某个计划形状,但引擎可能仍会决定在运行时采用不同的锁定。如果您包含上述TOP (1)
元素,则机会相当低。
以这种方式使用堆表有点不寻常。您应该考虑将其转换为聚簇表的优点,也许使用 Dan Guzman 在评论中建议的索引:
CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);
Run Code Online (Sandbox Code Playgroud)
这可能具有重要的空间重用优势,并为当前的死锁问题提供了一个很好的解决方法。
MERGE
在高并发环境中也有点不寻常。有点违反直觉,执行单独的INSERT
andUPDATE
语句通常更有效,例如:
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;
Run Code Online (Sandbox Code Playgroud)
请注意不再需要 RID 查找:
如果您可以保证存在唯一索引ItemKey
(如问题中所述)TOP (1)
,则UPDATE
可以删除 中的冗余,给出更简单的计划:
双方INSERT
并UPDATE
计划资格在任何情况下微不足道的计划。MERGE
总是需要完全基于成本的优化。
请参阅相关的问答SQL Server 2014 并发输入问题以了解要使用的正确模式,以及有关MERGE
.
死锁并不总是可以避免的。通过仔细的编码和设计,它们可以减少到最小,但应用程序应该始终准备好优雅地处理奇怪的死锁(例如,重新检查条件然后重试)。
如果您可以完全控制访问相关对象的进程,您还可以考虑使用应用程序锁来序列化对单个元素的访问,如SQL Server 并发插入和删除中所述。
归档时间: |
|
查看次数: |
7463 次 |
最近记录: |