为什么我的 MERGE 查询在序列化隔离级别事务中死锁?

Ste*_*idi 5 deadlock merge sql-server-2014

我试图避免我的MERGE查询出现死锁,它可能被不同的线程调用,并且可能在执行过程中使用相同的参数重叠。我对这个查询的体验与这个问题中描述的场景非常相似,我在下面列出了查询以供参考。

CREATE PROCEDURE MergeIt
    @dataToMerge MyTableType READONLY
AS
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    SET XACT_ABORT ON;

    BEGIN TRANSACTION

        MERGE INTO TargetTable WITH(HOLDLOCK) AS [target]
        USING @dataToMerge AS [source]
        ON [source].KeyPart_1 = [target].KeyPart_1 AND
           [source].KeyPart_2 = [target].KeyPart_2
        WHEN NOT MATCHED THEN
            INSERT(Data, KeyPart_1, KeyPart_2)
            VALUES([source].Data, [source].KeyPart_1, [source].KeyPart_2)
        WHEN MATCHED THEN
            UPDATE SET [target].Data = [source].Data,
                       [target].KeyPart_1 = [source].KeyPart_1,
                       [target].KeyPart_2 = [source].KeyPart_2;

    COMMIT TRANSACTION
RETURN 0
Run Code Online (Sandbox Code Playgroud)

TargetTable具有作为主键的标识列,并且对[KeyPart_1, KeyPart_2]列元组具有唯一性约束。 MyTableType具有类似于列元组的架构,TargetTable并且还定义了一个主键[KeyPart_1, KeyPart_2]

我试图确保MERGE在任何给定时间只允许一个进程运行此查询,并且我认为SERIALIZABLE隔离级别会强制执行此操作。然而,情况似乎并非如此。我已经捕获了这些XML 日志事件,它们显示了在死锁期间哪些资源和锁在起作用。一个查询有一个排他锁 (X),另一个有更新锁 (U)。当我输入这个时,我看到没有必要更新子句中的[KeyPart_1, KeyPart_2]列元组UPDATE,这很可能会导致死锁,因为该元组将触发索引更新。

关于如何解决这个问题还有其他建议吗?我想我可以盲目地尝试TABLOCKX用作表提示,但我想了解SERIALIZABLE隔离级别是如何在这里失败的。

谢谢!

Vla*_*nov 1

如果您想确保MERGE在任何给定时间只允许一个进程运行此查询(存储过程),那么这 sp_getapplock 是一个不错的选择。它非常简单,易于理解和维护,而不是晦涩的查询提示。我并不是说通过提示不可能达到同样的效果。对我来说更容易理解简单的互斥体。

这是我使用的存储过程的模板。根据需要调整超时。调用者应该意识到可能的超时并在需要时重试。

CREATE PROCEDURE MergeIt
    @dataToMerge MyTableType READONLY
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRANSACTION
    BEGIN TRY

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'MergeIt_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            MERGE INTO TargetTable WITH(HOLDLOCK) AS [target]
            USING @dataToMerge AS [source]
            ON [source].KeyPart_1 = [target].KeyPart_1 AND
               [source].KeyPart_2 = [target].KeyPart_2
            WHEN NOT MATCHED THEN
                INSERT(Data, KeyPart_1, KeyPart_2)
                VALUES([source].Data, [source].KeyPart_1, [source].KeyPart_2)
            WHEN MATCHED THEN
                UPDATE SET [target].Data = [source].Data
            ;

        END ELSE BEGIN
            -- timeout waiting for the lock
            -- TODO: handle the problem, e.g. return some error code,
            -- indicating that the caller should retry.
        END;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
        -- TODO: handle the problem. Return some error code?
    END CATCH;

    RETURN <the error code>
END
Run Code Online (Sandbox Code Playgroud)