web-dev-qa-db-ja.com

行のセットを分離するための更新がデッドロックになるのはなぜですか?

更新:楽観的ロック(スナップショット分離)に切り替えることで問題を解決しました。そもそもなぜそれが必要だったのかという疑問は未解決のままですが、私は先に進み、この時点で純粋に学術的な問題を検討する必要がありました。

理解できないストアドプロシージャの個別のインスタンス間でデッドロックが発生しています。

私のシステムは、3つの工場の製造機械からデータを収集します。さまざまなダッシュボードがこのデータベースからデータを選択しますが、更新/挿入は主に単一のストアドプロシージャ(saveCounts)を通じて行われます。各プラントには、この手順を1分に1回呼び出すスクリプトがあり、そのプラントのマシンの新しいデータを渡します。手順は約700行で、一連のクエリで構成されているため、ここではすべてを説明することはしません。以下のデッドロックレポートに示すように、デッドロックを生成している部分は、テーブルcountLogLastに対するいくつかの更新です。

関連するテーブルは次のようになります。

  1. countLogLast
    • 各マシンの最新情報を格納する小さなテーブル(<200行)
    • 主キーはmachineID INTです
  2. countLog
    • 数百万行の履歴データを含むテーブル
    • 主キーは(machineID INT、countStamp DATETIME)
  3. @shortStopDowntimeおよび@falseDowntime
    • 関連するプラントのマシンのサブセットを格納する小さなテーブル変数(<10行)
    • 主キーはmachineID INTです

デッドロックグラフとXMLレポートは次のとおりです。

deadlock graph

<deadlock>
 <victim-list>
  <victimProcess id="process8200f3868" />
 </victim-list>
 <process-list>
  <process id="process8200f3868" taskpriority="0" logused="6152" waitresource="KEY: 7:72057594089504768 (8194443284a0)" waittime="205" ownerId="95950131" transactionname="user_transaction" lasttranstarted="2020-02-24T06:35:03.737" XDES="0x8dca5740" lockMode="U" schedulerid="2" kpid="10416" status="suspended" spid="60" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2020-02-24T06:35:03.733" lastbatchcompleted="2020-02-24T06:35:03.733" lastattention="1900-01-01T00:00:00.733" hostname="ELGSQL01" hostpid="6376" loginname="SPC" isolationlevel="read uncommitted (1)" xactid="95950131" currentdb="7" lockTimeout="4294967295" clientoption1="673316896" clientoption2="128056">
   <executionStack>
    <frame procname="Andon.dbo.saveCounts" line="428" stmtstart="37104" stmtend="37906" sqlhandle="0x03000700c215e36c43eba20068ab000001000000000000000000000000000000000000000000000000000000">
        UPDATE old
        SET
            lastDownStamp = c.countStamp
        FROM
            countLogLast AS old
            JOIN countLog AS c ON c.machineID = old.machineID
        WHERE
            old.machineID IN (SELECT machineID FROM @shortStopDowntime) AND
            c.countStamp = (
                SELECT MAX(c2.countStamp)
                FROM countLog AS c2
                WHERE
                    c2.machineID = c.machineID AND
                    c2.reasonID != @REASON_RUNNING
            )
    </frame>
    <frame procname="adhoc" line="31" stmtstart="5116" sqlhandle="0x020000003f92ac38a5f767af9f699bc89e1c4b0c19c05dff0000000000000000000000000000000000000000">
EXEC dbo.saveCounts @countTable, @P109    </frame>
    <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown    </frame>
   </executionStack>
   <inputbuf>
(@P1 varchar(2),@P2 varchar(1),@P3 varchar(1),@P4 varchar(1),@P5 varchar(2),@P6 varchar(1),@P7 varchar(1),@P8 varchar(1),@P9 varchar(2),@P10 varchar(1),@P11 varchar(1),@P12 varchar(1),@P13 varchar(2),@P14 varchar(1),@P15 varchar(1),@P16 varchar(1),@P17 varchar(2),@P18 varchar(2),@P19 varchar(2),@P20 varchar(1),@P21 varchar(2),@P22 varchar(1),@P23 varchar(1),@P24 varchar(1),@P25 varchar(2),@P26 varchar(3),@P27 varchar(1),@P28 varchar(1),@P29 varchar(2),@P30 varchar(3),@P31 varchar(2),@P32 varchar(1),@P33 varchar(2),@P34 varchar(1),@P35 varchar(1),@P36 varchar(1),@P37 varchar(2),@P38 varchar(3),@P39 varchar(1),@P40 varchar(1),@P41 varchar(2),@P42 varchar(1),@P43 varchar(1),@P44 varchar(1),@P45 varchar(2),@P46 varchar(1),@P47 varchar(1),@P48 varchar(1),@P49 varchar(2),@P50 varchar(1),@P51 varchar(1),@P52 varchar(1),@P53 varchar(2),@P54 varchar(1),@P55 varchar(1),@P56 varchar(1),@P57 varchar(2),@P58 varchar(1),@P59 varchar(1),@P60 varchar(1),@P61 varchar(2),@P62 varchar(1),@P63 varchar(1),@P64 varchar(1),@P65 va   </inputbuf>
  </process>
  <process id="process276716558" taskpriority="0" logused="12640" waitresource="KEY: 7:72057594089504768 (879d521ac67a)" waittime="205" ownerId="95950768" transactionname="user_transaction" lasttranstarted="2020-02-24T06:35:10.063" XDES="0x274147740" lockMode="U" schedulerid="4" kpid="9984" status="suspended" spid="56" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2020-02-24T06:35:09.950" lastbatchcompleted="2020-02-24T06:35:09.950" lastattention="1900-01-01T00:00:00.950" hostname="ELGSQL01" hostpid="3060" loginname="SPC" isolationlevel="read uncommitted (1)" xactid="95950768" currentdb="7" lockTimeout="4294967295" clientoption1="673316896" clientoption2="128056">
   <executionStack>
    <frame procname="Andon.dbo.saveCounts" line="353" stmtstart="29600" stmtend="34580" sqlhandle="0x03000700c215e36c43eba20068ab000001000000000000000000000000000000000000000000000000000000">
        UPDATE old
        SET
            lastDownReasonID = c.reasonID,
            lastDownStamp = c.countStamp
        FROM
            countLogLast AS old
            JOIN countLog AS c ON c.machineID = old.machineID
        WHERE
            old.machineID IN (SELECT machineID FROM @falseDowntime) AND
            c.countStamp = (
                SELECT MAX(c2.countStamp)
                FROM countLog AS c2
                WHERE
                    c2.machineID = c.machineID AND
                    c2.reasonID != @REASON_RUNNING
            )
    <frame procname="adhoc" line="63" stmtstart="11452" sqlhandle="0x02000000a477fc2448c590d65bbade781893c643435fa3390000000000000000000000000000000000000000">
EXEC dbo.saveCounts @countTable, @P237    </frame>
    <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown    </frame>
   </executionStack>
   <inputbuf>
(@P1 varchar(2),@P2 varchar(1),@P3 varchar(1),@P4 varchar(1),@P5 varchar(2),@P6 varchar(2),@P7 varchar(2),@P8 varchar(1),@P9 varchar(2),@P10 varchar(1),@P11 varchar(1),@P12 varchar(1),@P13 varchar(2),@P14 varchar(3),@P15 varchar(3),@P16 varchar(1),@P17 varchar(2),@P18 varchar(1),@P19 varchar(1),@P20 varchar(1),@P21 varchar(2),@P22 varchar(1),@P23 varchar(1),@P24 varchar(1),@P25 varchar(2),@P26 varchar(1),@P27 varchar(1),@P28 varchar(1),@P29 varchar(2),@P30 varchar(1),@P31 varchar(1),@P32 varchar(1),@P33 varchar(2),@P34 varchar(1),@P35 varchar(3),@P36 varchar(1),@P37 varchar(2),@P38 varchar(1),@P39 varchar(1),@P40 varchar(1),@P41 varchar(2),@P42 varchar(1),@P43 varchar(1),@P44 varchar(1),@P45 varchar(2),@P46 varchar(3),@P47 varchar(3),@P48 varchar(1),@P49 varchar(2),@P50 varchar(1),@P51 varchar(1),@P52 varchar(1),@P53 varchar(2),@P54 varchar(1),@P55 varchar(1),@P56 varchar(1),@P57 varchar(2),@P58 varchar(2),@P59 varchar(2),@P60 varchar(1),@P61 varchar(2),@P62 varchar(2),@P63 varchar(1),@P64 varchar(1),@P65 va   </inputbuf>
  </process>
 </process-list>
 <resource-list>
  <keylock hobtid="72057594089504768" dbid="7" objectname="Andon.dbo.countLogLast" indexname="1" id="lock650ca4c00" mode="U" associatedObjectId="72057594089504768">
   <owner-list>
    <owner id="process276716558" mode="U" />
   </owner-list>
   <waiter-list>
    <waiter id="process8200f3868" mode="U" requestType="wait" />
   </waiter-list>
  </keylock>
  <keylock hobtid="72057594089504768" dbid="7" objectname="Andon.dbo.countLogLast" indexname="1" id="lock27baa6780" mode="X" associatedObjectId="72057594089504768">
   <owner-list>
    <owner id="process8200f3868" mode="X" />
   </owner-list>
   <waiter-list>
    <waiter id="process276716558" mode="U" requestType="wait" />
   </waiter-list>
  </keylock>
 </resource-list>
</deadlock>

デッドロック内の2つのUPDATEクエリを手動で(上記のXMLレポートと同じ順序で)実行すると、実際のクエリプランは次のようになります。

shortStopDowntimePlan

falseDowntimePlan

プロシージャ内のすべてのクエリが入力マシン(またはそのサブセット)でのみ動作し、スクリプトの3つのインスタンスがプロシージャの相互に排他的なマシンセットにフィードする場合、どのようにしてデッドロックが発生する可能性がありますか?デッドロックレポートを見ると、ページロックまたはオブジェクトロックではなく、キーロックが表示されます。クエリプランを見ると、countLogLastはスキャンされておらず、シークされているだけなので、無関係な行がロックされる理由がわかりません。何を理解していないのですか?

ソリューションに関する限り、私はスナップショット分離を有効にし、この手順でそれを使用することに傾倒しています。なぜなら、私は実際にはデータの競合がないことを知っているからです(競合をロックするだけ)、したがって、スナップショットは害を及ぼすことなく、これらのプロシージャインスタンス間の完全な同時実行性を可能にする必要があります。でも、そもそもなぜこの問題が起こっているのかわからないし、他の提案にも興味があります。

アップデート1

ジョシュダーネルは、以下の彼の回答の中で、前の呼び出しが終了する前に、所与のプラントがこの手順を再度呼び出していることが問題である可能性があることを賢く指摘しました。以下では、このsaveCountsプロシージャの最初から、その可能性を防ぐための抜粋を追加しましたが、おそらくそうではありませんか?

これは、sp_getapplockを使用して各マシンをロックすることです。私がこれを行った理由は、これらのテーブルへのもう1つの更新ソース(別のストアドプロシージャ)がこのプロシージャと同時に特定のマシンのデータを操作するのを防ぐためです。このスキームのボーナス特典は、特定のプラントのsaveCountsプロシージャが関連するマシンのロックを取得するのを待機して、私が見ているデッドロックを防ぐことになると思います。

この手順の実行に214秒もかかっていること、各プラントのスクリプトが60秒ごとに呼び出すこと、およびsp_getapplockに20秒のタイムアウトを設定していることを考えると、同じプラントが重複する場合、これらのデッドロックではなくsp_getapplockタイムアウトが発生します。ループする前にプロシージャが完了するまで、スクリプトは単にブロックしていると思います。つまり、60秒を超える散発的な実行時間は問題ではありません。タイムアウトの欠如はその仮定をサポートするようですが、私はそれを調べます。

ALTER PROCEDURE dbo.saveCounts
    @counts type_countTableInput READONLY,

...

BEGIN TRANSACTION

    DECLARE
        @machineID INT,
        @lockName VARCHAR(25),
        @errorMsg VARCHAR(MAX),
        @result INT;

    DECLARE cursorMachine CURSOR FOR SELECT machineID FROM @counts;

    OPEN cursorMachine;
    FETCH NEXT FROM cursorMachine INTO @machineID;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @lockName = 'downtimeReasonLock' + CAST(@machineID AS VARCHAR(25))

        -- Attempt to take the lock.
        EXEC @result = sp_getapplock
                        @Resource = @lockName,
                        @LockMode = 'Exclusive',
                        @LockOwner = 'Transaction',
                        @LockTimeout = 20000,
                        @DbPrincipal = 'public'
        IF @result < 0  -- if there was a problem getting the lock
        BEGIN
            SELECT @errorMsg =
                'Error getting ' + @lockName +
                '.  sp_getapplock returned ' + CAST(@result AS VARCHAR(25)) + ': ' +
                CASE @result
                    --WHEN  1  THEN 'The lock was granted successfully after waiting for other incompatible locks to be released.' (I left this here to show the one other possible return value, but it's impossible to get it here due to the IF statement above.)
                    WHEN   -1  THEN 'The lock request timed out.'
                    WHEN   -2  THEN 'The lock request was canceled.'
                    WHEN   -3  THEN 'The lock request was chosen as a deadlock victim.'
                    WHEN -999  THEN 'Indicates a parameter validation or other call error.'
                    ELSE            'Unexpected return value'
                END
            RAISERROR(@errorMsg, 16, 1)
        END

        FETCH NEXT FROM cursorMachine INTO @machineID;
    END;

    CLOSE cursorMachine;
    DEALLOCATE cursorMachine;

アップデート2

Pythonこのプロシージャを呼び出すスクリプトは、過去のインスタンスを実行している間に再度呼び出されるのではなく、プロシージャが戻るまで待機するだけであることを確認しました。スクリプトからさらに多くの診断データを収集しました。スクリプトが何らかの方法で同じマシンのセットに対して同時に2回以上プロシージャを呼び出しているという証拠はありません。したがって、私は元の仮定に戻ります。 -マシンの重複セットが何らかの理由でデッドロックを引き起こしています。

更新3:

スナップショット分離を有効にし、この手順を使用するように設定しました。毎日数十のデッドロックが発生するのではなく、数日後の時点で単一のデッドロックがまだ発生していないので、問題ないと思います。欠点は、この手順のパフォーマンスが以前よりもはるかに悪いことです。

  • 平均労働時間は3倍前の値です
  • 平均経過時間は以前の値の3倍
  • 最大経過時間は前の値の2.5倍です:600秒(わかりません...)
3
MarredCheese

実行計画を見ると、そのプロシージャを同時に実行してもデッドロックが発生してはならないという結論に反することは困難です。彼らは、夜に通過する船のように、countLogLatestテーブルにあるさまざまなキーのキーロックを解除する必要があります。


あなたが示したデッドロックを引き起こす可能性がある1つのことは、同じプラントのデータに対してプロシージャが同時に複数回実行されている場合です。あなたは言及しました:

この手順を1分に1回呼び出す各プラントのスクリプトがあります...

スクリプトが何らかの理由で1分以上かかる場合、次の反復が実行され、最初の反復がデッドロックする可能性があります。または、プラントの1つでスクリプトのスケジュールにバグがあり、スクリプトが2回起動するのと同じくらい簡単かもしれません。

これが問題である場合、最善の解決策は、おそらく1分未満で実行されるようにprocを調整するか、タスクスケジューリング側で実行が重複しないようにすることです。


指摘する価値のあるもう1つの項目は、デッドロックしたクエリがREAD UNCOMMITTED分離レベルで実行されていることです。

あなたが示した特定の実行計画で問題を引き起こすことを想像するのは難しいです。ただし、これはより大きなプロシージャの一部であると述べたので、プロシージャ全体がこのレベルで実行されている場合、これらのクエリにつながる奇妙な同時実行性の要素(テーブル変数など)につながることは容易に想像できます。

一般的に言えば、READ UNCOMMITTEDは、私に好奇心をかきたてます。これをREAD COMMITTEDに変更して、UPDATEデッドロックが解消されるかどうかを確認することをお勧めします。


デッドロックの一般的な原因は、複数のセッションにわたって異なる順序で同じデータを処理することです。カーソルクエリにORDER BY句を追加して、この問題のリスクを軽減する必要があります。

DECLARE cursorMachine CURSOR FOR SELECT machineID FROM @counts ORDER BY machineID;

あなたのポイントに、私は一種の「順不同」の処理があなたの説明に基づいてアプリロックのタイムアウトを引き起こすことを期待していますただし、ORDER BYを追加しても害はなく、デッドロックを解決できます。

ちなみに、あなたが共有したコードに基づいて、トランザクションがコミットするまで、各セッションは一度に1つずつマシンのアプリロックを「累積」します(sp_releaseapplockを呼び出さないため)。多分これは意図的なものか、実際のコードはそのマシンの処理後にロックを解放しますが、私はそれについて言及したいと思いました。

1
Josh Darnell