web-dev-qa-db-ja.com

シリアル化された分離レベルのトランザクションでMERGEクエリがデッドロックするのはなぜですか?

私は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

TargetTableには、主キーとして機能するID列があり、さらに[KeyPart_1, KeyPart_2]列タプルに一意性制約があります。 MyTableTypeTargetTableと同様のスキーマを持ち、[KeyPart_1, KeyPart_2] column-Tupleの主キーも定義します。

このMERGEクエリを実行できるプロセスが1つだけであることを確認しようとしています。SERIALIZABLE分離レベルがこれを強制していると思いました。ただし、そうではないようです。私はこれらをキャプチャしました XMLログイベント デッドロック中にどのリソースとロックが機能しているかを示します。 1つのクエリには排他ロック(X)があり、もう1つのクエリには更新ロック(U)があります。これを入力していると、UPDATE句の[KeyPart_1, KeyPart_2] column-Tupleを更新する必要がないことがわかります。このタプルは、インデックスの更新。

これを解決する方法について他に提案はありますか?私は盲目的にTABLOCKXをテーブルヒントとして使用することができると思いますが、ここでSERIALIZABLE分離レベルがどのように失敗したかを理解したいと思います。

ありがとう!

3
Steve Guidi

特定の時点でこのMERGEクエリ(ストアドプロシージャ)の実行を1つのプロセスのみに許可する場合は、 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
1