合并语句自身陷入僵局


22

我有以下过程(SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId,UserId,MyKey构成目标表的组合键。CompanyId是父表的外键。此外,上还有一个非聚集索引CompanyId asc, UserId asc

它是从许多不同的线程中调用的,而在调用同一条语句的不同进程之间,我始终遇到死锁。我的理解是“ with(holdlock)”对于防止插入/更新竞争条件错误是必需的。

我假设两个不同的线程在验证约束时以不同的顺序锁定行(或页面),从而导致死锁。

这是正确的假设吗?

解决这种情况的最佳方法是什么(即无死锁,对多线程性能的影响最小)?

查询计划图片 (如果在新选项卡中查看图像,则该图像可读。抱歉,尺寸太小。)

  • @datatable中最多有28行。
  • 我已经追溯了代码,但看不到我们在此处开始交易的任何地方。
  • 外键设置为仅在删除时级联,并且父表中没有删除。

Answers:


12

好吧,看了几次之后,我认为您的基本假设是正确的。这里可能发生的是:

  1. MERGE的MATCH部分检查索引是否匹配,并随即对这些行/页面进行读锁定。

  2. 当它有不匹配的行时,它将尝试首先插入新的索引行,因此它将请求对行/页进行写锁定...

但是,如果另一个用户也到达同一行/页面上的第1步,则第一个用户将被阻止进行更新,并且...

如果第二个用户也需要在同一页面上插入,则说明他们陷入了僵局。

AFAIK,只有一种(简单)的方法可以100%确保您不会在此过程中陷入僵局,这将是在TABLOCKX提示中添加MERGE,但这可能会对性能产生严重的影响。

可能是加入了TABLOCK提示,而不是将足以解决问题,而不必大对广告效果产生影响。

最后,您也可以尝试添加PAGLOCK,XLOCK或同时添加PAGLOCK和XLOCK。同样,这可能会起作用,并且性能可能不会太糟糕。您必须尝试一下才能看到。


您认为快照隔离级别(行版本控制)在这里可能有用吗?
Mikael Eriksson

也许。否则可能会将死锁异常转换为并发异常。
RBarryYoung

2
在作为INSERT语句目标的表上指定TABLOCK提示与指定TABLOCKX提示具有相同的效果。(来源:msdn.microsoft.com/en-us/library/bb510625.aspx
tuespetre's

31

如果表变量仅保留一个值,则不会有问题。对于多行,存在死锁的新可能性。假设两个并发进程(A&B)的表变量包含同一公司的(1、2)和(2、1)。

进程A读取目标,未找到任何行,并插入值“ 1”。它在值“ 1”上具有排他的行锁。进程B读取目标,找不到行,并插入值'2'。它在值'2'上拥有排他行锁。

现在,进程A需要处理第2行,而进程B需要处理第1行。这两个进程都无法取得进展,因为它需要的锁与另一个进程所持有的排他锁不兼容。

为了避免多行出现死锁,每次都需要以相同顺序处理(和访问表)行。问题中显示的执行计划中的表变量是堆,因此行没有内在顺序(尽管不保证一定能以插入顺序读取它们):

现有计划

缺少一致的行处理顺序将直接导致死锁机会。第二个考虑因素是,缺少密钥唯一性保证意味着必须提供表后台处理程序才能提供正确的万圣节保护。假脱机程序是一个急切的假脱机程序,这意味着所有行都将被写入tempdb工作表,然后再读取并重播给Insert运算符。

重新定义TYPEtable变量以包含一个集群PRIMARY KEY

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

现在,执行计划显示了对聚集索引的扫描,并且唯一性保证意味着优化器能够安全地删除表假脱机:

带主键

MERGE128个线程上对该语句进行5000次迭代的测试中,集群表变量未发生死锁。我应该强调,这仅仅是基于观察;聚簇表变量也可以(技术上)以各种顺序产生其行,但是一致顺序的机会大大增加了。当然,对于每个新的累积更新,Service Pack或新版本的SQL Server,都需要对观察到的行为进行重新测试。

如果无法更改表变量定义,则还有另一种选择:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

这也消除了假脱机(和行顺序一致性),但以引入显式排序为代价:

排序计划

使用相同的测试,该计划也没有产生僵局。复制脚本如下:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;

8

我认为SQL_Kiwi提供了很好的分析。如果您需要解决数据库中的问题,则应遵循他的建议。当然,您每次升级,应用Service Pack或添加/更改索引或索引视图时,都需要重新测试它是否仍然适用。

还有其他三种选择:

  1. 您可以序列化插入,以免它们冲突:您可以在事务开始时调用sp_getapplock,并在执行MERGE之前获取排他锁。当然,您仍然需要对其进行压力测试。

  2. 您可以让一个线程处理所有插入,以便您的应用服务器处理并发。

  3. 死锁后,您可以自动重试-如果并发性很高,这可能是最慢的方法。

无论哪种方式,只有您才能确定解决方案对性能的影响。

尽管我们确实有很多潜在的死锁,但通常我们的系统中根本没有死锁。在2011年,我们在一次部署中犯了一个错误,在几个小时内发生了六个死锁,所有死锁都遵循相同的场景。我很快就解决了这一点,这就是这一年的所有僵局。

我们在系统中主要使用方法1。对我们来说真的很好。


-1

另一种可能的方法-我发现Merge有时会出现锁定和性能问题-最好使用Option(MaxDop x)查询选项

在昏暗和遥远的过去,SQL Server提供了“插入行级别锁定”选项-但这似乎已经死了,但是具有身份的群集PK应该使插入运行干净。

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.