Merge 语句本身死锁

Sak*_*o73 24 sql-server deadlock sql-server-2008-r2 merge

我有以下过程(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;
Run Code Online (Sandbox Code Playgroud)

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

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

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

这是一个正确的假设吗?

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

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

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

Pau*_*ite 35

如果表变量只保存一个值,就不会有问题。对于多行,出现死锁的新可能性。假设两个并发进程(A 和 B)使用包含同一公司的 (1, 2) 和 (2, 1) 的表变量运行。

进程 A 读取目的地,未找到任何行,并插入值“1”。它持有值“1”的排他行锁。进程 B 读取目标,未找到任何行,并插入值“2”。它持有值“2”的排他行锁。

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

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

现有计划

缺乏一致的行处理顺序直接导致死锁机会。第二个考虑因素是缺少密钥唯一性保证意味着必须使用 Table Spool 来提供正确的万圣节保护。假脱机是一个急切假脱机,这意味着所有行在被读回和为插入操作员重放之前都被写入tempdb工作表。

重新定义TYPE表变量的 以包含聚集的PRIMARY KEY

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

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

带主键

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);
Run Code Online (Sandbox Code Playgroud)

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

排序计划

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

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;
Run Code Online (Sandbox Code Playgroud)


RBa*_*ung 12

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

  1. MERGE 的 MATCH 部分检查索引是否匹配,并在运行时读取锁定这些行/页面。

  2. 当它有一个没有匹配的行时,它会先尝试插入新的索引行,这样它就会请求一个行/页写锁......

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

如果第二个用户也需要在同一页面上插入,那么他们就会陷入僵局。

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

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

最后,您还可以尝试添加 PAGLOCK、XLOCK 或 PAGLOCK 和 XLOCK。同样,这可能会奏效,而且性能可能不会太糟糕。你得试试看。

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

A-K*_*A-K 8

我认为 SQL_Kiwi 提供了非常好的分析。如果你需要解决数据库中的问题,你应该按照他的建议去做。当然,每次升级、应用服务包或添加/更改索引或索引视图时,您都需要重新测试它是否仍然适用于您。

还有其他三种选择:

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

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

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

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

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

我们主要在我们的系统中使用方法 1。它对我们非常有效。