INSERT中にテーブルのデッドロックに苦しんでいます。これはマルチテナントデータベースであり、Read Committed Snapshot Isolation(RCSI)が有効になっています。
INSERTにチェック制約があり、スカラー値関数を実行して結果1をチェックする予約(イベントに関係なくsmalldatetimeによる)が重複しないようにします。この制約は、チェックするREADCOMMITTEDLOCKヒントで同じテーブルを検索します。 ID(PK /クラスター化インデックス)が新しく挿入された行のIDと等しくない場合のロジック違反。
READCOMMITTEDLOCKヒントが使用されたのは、RCSIが有効になっていて、行をスキップしないようにする必要があるためです。これにより、予約が重複する可能性があります。
制約は、デッドロックの原因となるインデックスidx_report_foobarに対してINDEX SEEKを実行します。
どんな援助でも大歓迎です。
XMLは次のとおりです(データベースにあるテーブルフィールドのロジックと名前の一部を削除するように調整されています)。
<deadlock>
<victim-list>
<victimProcess id="process91591c108" />
</victim-list>
<process-list>
<process id="process91591c108" taskpriority="0" logused="1328" waitresource="KEY: 9:72057594095861760 (c2e966d5eb6a)" waittime="3046" ownerId="2628292921" transactionname="user_transaction" lasttranstarted="2018-03-09T14:24:13.820" XDES="0x708a80d80" lockMode="S" schedulerid="10" kpid="8964" status="suspended" spid="119" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2018-03-09T14:24:13.823" lastbatchcompleted="2018-03-09T14:24:13.820" lastattention="1900-01-01T00:00:00.820" clientapp=".Net SqlClient Data Provider" hostname="SERVERNAMEHERE" hostpid="33672" loginname="DOMAIN\USERHERE" isolationlevel="read committed (2)" xactid="2628292921" currentdb="9" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="mydb.dbo.CheckForDoubleBookings" line="12" stmtstart="920" stmtend="3200" sqlhandle="0x0300090018ef9b72531bea009ea8000000000000000000000000000000000000000000000000000000000000">
IF EXISTS (SELECT *
FROM dbo.bookings a WITH (READCOMMITTEDLOCK)
WHERE a.id <> @id
AND a.userID = @userID
AND @bookingStart < a.bookingEnd
AND a.bookingStart < @bookingEnd
AND a.eventID = @eventID
</frame>
<frame procname="adhoc" line="1" stmtstart="288" stmtend="922" sqlhandle="0x020000005ed9af11c02db2af69df1d5fb6d1adb0e4812afb0000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
(@0 datetime2(7),@1 datetime2(7),@2 int,@3 int,@4 int,@5 int,@6 int,@7 nvarchar(4000),@8 datetime2(7),@9 nvarchar(50),@10 int,@11 nvarchar(255))INSERT [dbo].[bookings]([bookingStart], [bookingEnd], [userID], [eventID], [TypeId], [Notes], [Timestamp], [AddedById])
VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, NULL, @9, @10, @11, NULL, NULL)
SELECT [Id]
FROM [dbo].[bookings]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity() </inputbuf>
</process>
<process id="processca27768c8" taskpriority="0" logused="1328" waitresource="KEY: 9:72057594095861760 (3ba50d420e66)" waittime="3048" ownerId="2628280537" transactionname="user_transaction" lasttranstarted="2018-03-09T14:24:04.063" XDES="0xa555403b0" lockMode="S" schedulerid="6" kpid="12776" status="suspended" spid="124" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2018-03-09T14:24:04.070" lastbatchcompleted="2018-03-09T14:24:04.063" lastattention="1900-01-01T00:00:00.063" clientapp=".Net SqlClient Data Provider" hostname="SERVERNAMEHERE" hostpid="33672" loginname="DOMAIN\USERHERE" isolationlevel="read committed (2)" xactid="2628280537" currentdb="9" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="mydb.dbo.CheckForDoubleBookings" line="12" stmtstart="920" stmtend="3200" sqlhandle="0x0300090018ef9b72531bea009ea8000000000000000000000000000000000000000000000000000000000000">
IF EXISTS (SELECT *
FROM dbo.bookings a WITH (READCOMMITTEDLOCK)
WHERE a.id <> @id
AND a.userID = @userID
AND @bookingStart < a.bookingEnd
AND a.bookingStart < @bookingEnd
AND a.eventID = @eventID
</frame>
<frame procname="adhoc" line="1" stmtstart="288" stmtend="922" sqlhandle="0x020000005ed9af11c02db2af69df1d5fb6d1adb0e4812afb0000000000000000000000000000000000000000">
unknown </frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
unknown </frame>
</executionStack>
<inputbuf>
(@0 datetime2(7),@1 datetime2(7),@2 int,@3 int,@4 int,@5 int,@6 int,@7 nvarchar(4000),@8 datetime2(7),@9 nvarchar(50),@10 int,@11 nvarchar(255))INSERT [dbo].[bookings]([bookingStart], [bookingEnd], [userID], [eventID], [TypeId], [Notes], [Timestamp], [AddedById])
VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, NULL, @9, @10, @11, NULL, NULL)
SELECT [Id]
FROM [dbo].[bookings]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity() </inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594095861760" dbid="9" objectname="mydb.dbo.bookings" indexname="idx_report_foobar" id="locke83fdbe80" mode="X" associatedObjectId="72057594095861760">
<owner-list>
<owner id="processca27768c8" mode="X" />
</owner-list>
<waiter-list>
<waiter id="process91591c108" mode="S" requestType="wait" />
</waiter-list>
</keylock>
<keylock hobtid="72057594095861760" dbid="9" objectname="mydb.dbo.bookings" indexname="idx_report_foobar" id="lock7fdb48480" mode="X" associatedObjectId="72057594095861760">
<owner-list>
<owner id="process91591c108" mode="X" />
</owner-list>
<waiter-list>
<waiter id="processca27768c8" mode="S" requestType="wait" />
</waiter-list>
</keylock>
</resource-list>
</deadlock>
インデックス:
CREATE NONCLUSTERED INDEX [idx_report_foobar] ON [dbo].[bookings]
(
[eventID] ASC
)
INCLUDE ( [bookingStart],
[bookingEnd],
[userID]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80)
GO
スカラー関数は、ユーザーが二重予約できないようにするために使用されます(イベントIDに関係なく、日時は重複しません)。
BEGIN
DECLARE @Valid bit = 1;
IF EXISTS (SELECT *
FROM dbo.bookings a WITH (READCOMMITTEDLOCK)
WHERE a.id <> @id
AND a.userID = @userID
AND @bookingStart < a.bookingEnd
AND a.bookingStart < @bookingEnd
AND a.eventID = @eventID)
SET @Valid = 0;
RETURN @Valid;
END;
dbo.CheckForDoubleBookings
スカラーUDFに渡されるパラメーター値が利用可能である場合、これを絞り込む方が簡単ですが、ここには十分な情報があり、経験に基づいた推測を行うことができます。
戦われている2つの異なる行があります。プロセス自体は次のことを行います。
CHECK CONSTRAINT
が発生する:INSERT
ステートメントである明示的/暗黙的または自動コミットトランザクション内で、スカラーUDFを使用して条件を確認します。1
を返す場合、暗黙的にROLLBACK
および例外を発行します。COMMIT
したがって、2つの異なるセッションで同時に、同じuserID
に対して、ステップ1(上記)が発生し、行が挿入されますandは、その上で排他ロックを維持します行。次に、各セッションで、INSERT
を完了する前に、ステップ2が実行され、一致する可能性のある行をチェックするチェック制約が処理されます(4つのプロパティは同じですが、id
は異なります)。 「コミットされた」データのみを読み取っていますが、それぞれのチェック制約の検証を待っているため、新しい行はまだコミットされていません。
ここでの問題は、検証プロセスがワークフローの間違った時点/シーケンスで発生していることです。 CHECK CONSTRAINT
を介して処理されているため、行は既に追加されています(コミットされていないだけ)。これにより、スカラーUDFに渡すためにid
が割り当てられます。そして、プロセスは、一致するエントリが存在しないことが確実になるまでコミットしませんが、そのエントリも同じ理由でまだコミットされていないため、他のエントリを表示できません。
ここにいくつかのオプションがあります:
最も単純ですが、確かに理想的ではないオプションは、コミットされていない/ダーティなデータの読み取りに切り替えることです。この場合、現在のNOLOCK
ではなくREADCOMMITTEDLOCK
ヒントを使用するように切り替えます。ここでの問題は、ほとんどの場合は機能しますが、次のいずれかのシナリオになる可能性があることです。
INSERT
が中止されます。
どちらの場合も、どちらのエントリも保存されません。
CHECK CONSTRAINT
andを削除してみて、その非クラスタ化インデックスにIGNORE_DUP_KEY = ON
オプションを追加してください。これにより、両方が同時に発生した場合、一方がコミットし、もう一方が静かに失敗し(警告が表示されます)、何も挿入されないという状況が発生します。後で@@ROWCOUNT
をチェックするので、それは問題ないはずです。
要件は、インデックスがUNIQUE
である必要があることです。 eventID
自体が一意である場合は、UNIQUE
キーワードをインデックス定義に追加するだけです。 eventID
自体が一意でない場合は、一意性を確立できるまで、現在INCLUDE
列であるものをキー列に移動します。
このロジックはINSERT
ステートメントの外で処理します。一致する行が存在するかどうかを事前にテストできます。何も見つからない場合は、INSERT
を実行します。また、その「チェック」は、同じ値の別のINSERT
とまったく同じタイミングで実行されると失敗する場合があるため、失敗します。したがって、これはINSERT
をTRY...CATCH
構文でラップし、エラーが発生しても無視して処理されます。
DECLARE @NewID INT;
BEGIN TRY
IF (NOT EXISTS(SELECT * FROM dbo.Table WHERE columns = @parameters))
BEGIN
INSERT INTO dbo.Table (columns) VALUES (@parameters);
SET @NewID = SCOPE_IDENTITY();
END;
END TRY
BEGIN CATCH
-- Either return custom error, or handle in a different way, such as
-- selecting and returning the `id` matching the same criteria that
-- was the basis of the failed `INSERT`, such as:
SELECT @NewID = [id]
FROM dbo.Table
WHERE columns = @parameters
END CATCH;
@userID, @eventID
の組み合わせandの「開始」と「終了」の日付のバリエーションを許可する必要がある場合、@userID, @eventID
の組み合わせはnot一意です(したがって、上記のオプション#2は機能しません)app_lock
の使用を検討する必要がある場合があります。ここで、「リソース」は@userID, @eventID
の組み合わせまたはuserID
。これにより、一度に複数のロックが防止されますその組み合わせの場合。ただし、app_lock
は特定の文字列である「リソース」のみをロックし、それとは関係がないため、現在リクエストされている日付より前の過去の日付のテーブルにすでに存在する組み合わせは問題になりません。テーブル内のデータ(実際、「アプリロック」はテーブルを認識していません)。
ここでの考え方は、最初に「アプリロック」を作成してシングルスレッドを強制するこれだけuserID
なので、プロセスはa)重複する日付が存在するかどうかを確認し、b )重複する日付が見つからない場合は挿入します。同じuserID
の他のセッションは、「アプリロック」が解除されるまでブロックされます。その時点で、セッションは(一度に1つユーザーIDごとに)、重複する日付が存在するかどうかを確認します。
アプリロックの詳細については、以下の2つの回答を参照してください。ここでもDBA.SEにあります(どちらにも「アプリロック」ドキュメントへのリンクがあります)。
(この回答のコメントで)それが明確にされたので:
ユーザーは重複する予約を持つことはできません。
予約はeventIDに関連付けられており、異なる日付のeventIDを持つ複数のイベントが同じ日に発生する可能性があります。
userID, eventID
の組み合わせが一意であっても、オプション1、2、3はまったく機能しません。重複する日付についてここで本当にチェックされているのはuserID
だけだからです。オプション#4は残ります。これは、「リソース」が単にuserID
の場合に機能します。しかし、別の問題が提起されました:
INSERT
は、C#コードでEntity Frameworkによって生成されるため、独自に作成します。
...INSTEAD OF INSERT
ではなくCHECK CONSTRAINT
トリガーを使用することで解決できるでしょうか?これにより、データベーステーブルを手動で変更した場合でも、検証に従う必要があります。
確かに、このロジック(オプション3または4)をカプセル化できるようにロジックをストアドプロシージャに移動すると、すべてのコードパス(およびアドホック更新)がストアドプロシージャを使用した場合にのみ機能します。 INSTEAD OF INSERT
トリガーは、ルールに従った哲学的または道徳的不一致のためにストアドプロシージャの使用を拒否する悪質なサポートスタッフや開発者によって開始されたイベントを処理しますが、既存の「チェック」を使用できません論理。このようなトリガーは「行がすでに存在するため、他のセッションをブロックします」という問題を回避しますが、オプション3と同じ問題が発生し、両方のセッションで一致する行を確認して検出できず、重複する日付を挿入できます。
ただし、INSTEAD OF INSERT
トリガーのロジックとして「アプリロック」を使用すると機能します。また、トリガーはトランザクション内にすでに存在するため、その部分を手動で処理する必要はありません。プロセス/トリガーは次のようになります。
-- Either prevent multi-row inserts OR remove this "IF" block and wrap the rest
-- of the logic in a cursor that will process 1 row at a time from "inserted".
IF ((SELECT COUNT(*) FROM inserted) > 1)
BEGIN
ROLLBACK;
RAISERROR(N'Slow your roll, yo! One event at a time, ya dig?', 16, 1);
END;
DECLARE @Resource NVARCHAR(150);
SET @Resource = N'New Booking for: ' + CONVERT(NVARCHAR(20), @userID);
EXEC sys.sp_getapplock @Resource, other options;
IF (NOT EXISTS (SELECT *
FROM dbo.bookings a WITH (READCOMMITTEDLOCK)
WHERE a.id <> @id
AND a.userID = @userID
AND @bookingStart < a.bookingEnd
AND a.bookingStart < @bookingEnd
AND a.eventID = @eventID))
BEGIN
INSERT INTO dbo.bookings (columns) VALUES (@values);
END;
EXEC sys.sp_releaseapplock @Resource;
この方法で「アプリロック」を行うと、notが他のINSERT
sのuserID
sで競合を引き起こします。
Oracleでは、ロジックによってMutating Table
エラーが発生します。そこでの解決策はおそらく似ています。
ソリューション:すべてのDMLアクティビティをターゲットテーブルにシリアル化します。
推奨事項の詳細はRDBMS間で変更される可能性がありますが、疑似コードは変更されません。
疑似コード:
シリアル化ロックを取得する
変更されたデータがまだ有効であることを表明する
DMLを実行する
アプリケーションがテーブルに対して直接DMLアクティビティを実行することは許可されません。代わりに、一連のプロシージャを使用する必要があります。
使用手順によって、コードとロジックを再利用できるようになります。これは、異なる言語で記述されたアプリケーションが作成されるにつれて重要になります。
Oracle内のMutating Table
に対するほとんどのソリューションは、select ... for update
を介した行ロックを伴います。 @Solomonの回答に対するコメントは、MS-SQLがUPDLOCK
を使用して同じことを達成していることを示しているようです。
可能であれば、取得する行ロックの数を最小限に抑える必要があります。状況によっては、外部キーによって参照される親行を行ロックすることでこれを実現できます。あなたの状況では、おそらく、イベントの日/月ごとに、別のテーブルで行ロックを取得する必要があります。
他のシリアル化の方法には、名前付きミューテックスDBMS_LOCK
for Oracleの使用、またはテーブル全体のロックが含まれます。
MS-SQLの実際のコードを調整する必要があります。