削除/挿入時の同じ排他的にロックされたクラスター化キー(NHibernateを使用)でのSQLデッドロック


29

私はこのデッドロックの問題にかなりの数日取り組んでおり、私が何をしようとも、何らかの形で持続します。

まず、一般的な前提:1対多の関係でVisitItemsを使用した訪問があります。

VisitItems関連情報:

CREATE TABLE [BAR].[VisitItems] (
    [Id]                INT             IDENTITY (1, 1) NOT NULL,
    [VisitType]         INT             NOT NULL,
    [FeeRateType]       INT             NOT NULL,
    [Amount]            DECIMAL (18, 2) NOT NULL,
    [GST]               DECIMAL (18, 2) NOT NULL,
    [Quantity]          INT             NOT NULL,
    [Total]             DECIMAL (18, 2) NOT NULL,
    [ServiceFeeType]    INT   NOT NULL,
    [ServiceText]       NVARCHAR (200)  NULL,
    [InvoicingProviderId] INT   NULL,
    [FeeItemId]        INT             NOT NULL,
    [VisitId]          INT             NULL,
    [IsDefault] BIT NOT NULL DEFAULT 0, 
    [SourceVisitItemId] INT NULL, 
    [OverrideCode] INT NOT NULL DEFAULT 0, 
    [InvoiceToCentre] BIT NOT NULL DEFAULT 0, 
    [IsSurchargeItem] BIT NOT NULL DEFAULT 0, 
    CONSTRAINT [PK_BAR.VisitItems] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_BAR.VisitItems_BAR.FeeItems_FeeItem_Id] FOREIGN KEY ([FeeItemId]) REFERENCES [BAR].[FeeItems] ([Id]),
    CONSTRAINT [FK_BAR.VisitItems_BAR.Visits_Visit_Id] FOREIGN KEY ([VisitId]) REFERENCES [BAR].[Visits] ([Id]), 
    CONSTRAINT [FK_BAR.VisitItems_BAR.VisitTypes] FOREIGN KEY ([VisitType]) REFERENCES [BAR].[VisitTypes]([Id]), 
    CONSTRAINT [FK_BAR.VisitItems_BAR.FeeRateTypes] FOREIGN KEY ([FeeRateType]) REFERENCES [BAR].[FeeRateTypes]([Id]),
    CONSTRAINT [FK_BAR.VisitItems_CMN.Users_Id] FOREIGN KEY (InvoicingProviderId) REFERENCES [CMN].[Users] ([Id]),
    CONSTRAINT [FK_BAR.VisitItems_BAR.VisitItems_SourceVisitItem_Id] FOREIGN KEY ([SourceVisitItemId]) REFERENCES [BAR].[VisitItems]([Id]),
    CONSTRAINT [CK_SourceVisitItemId_Not_Equal_Id] CHECK ([SourceVisitItemId] <> [Id]),
    CONSTRAINT [FK_BAR.VisitItems_BAR.OverrideCodes] FOREIGN KEY ([OverrideCode]) REFERENCES [BAR].[OverrideCodes]([Id]),
    CONSTRAINT [FK_BAR.VisitItems_BAR.ServiceFeeTypes] FOREIGN KEY ([ServiceFeeType]) REFERENCES [BAR].[ServiceFeeTypes]([Id])
)

CREATE NONCLUSTERED INDEX [IX_FeeItem_Id]
    ON [BAR].[VisitItems]([FeeItemId] ASC)

CREATE NONCLUSTERED INDEX [IX_Visit_Id]
    ON [BAR].[VisitItems]([VisitId] ASC)

訪問情報:

CREATE TABLE [BAR].[Visits] (
    [Id]                     INT            IDENTITY (1, 1) NOT NULL,
    [VisitType]              INT            NOT NULL,
    [DateOfService]          DATETIMEOFFSET  NOT NULL,
    [InvoiceAnnotation]      NVARCHAR(255)  NULL ,
    [PatientId]              INT            NOT NULL,
    [UserId]                 INT            NULL,
    [WorkAreaId]             INT            NOT NULL, 
    [DefaultItemOverride] BIT NOT NULL DEFAULT 0, 
    [DidNotWaitAdjustmentId] INT NULL, 
    [AppointmentId] INT NULL, 
    CONSTRAINT [PK_BAR.Visits] PRIMARY KEY CLUSTERED ([Id] ASC),
    CONSTRAINT [FK_BAR.Visits_CMN.Patients] FOREIGN KEY ([PatientId]) REFERENCES [CMN].[Patients] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_BAR.Visits_CMN.Users] FOREIGN KEY ([UserId]) REFERENCES [CMN].[Users] ([Id]),
    CONSTRAINT [FK_BAR.Visits_CMN.WorkAreas_WorkAreaId] FOREIGN KEY ([WorkAreaId]) REFERENCES [CMN].[WorkAreas] ([Id]), 
    CONSTRAINT [FK_BAR.Visits_BAR.VisitTypes] FOREIGN KEY ([VisitType]) REFERENCES [BAR].[VisitTypes]([Id]),
    CONSTRAINT [FK_BAR.Visits_BAR.Adjustments] FOREIGN KEY ([DidNotWaitAdjustmentId]) REFERENCES [BAR].[Adjustments]([Id]), 
);

CREATE NONCLUSTERED INDEX [IX_Visits_PatientId]
    ON [BAR].[Visits]([PatientId] ASC);

CREATE NONCLUSTERED INDEX [IX_Visits_UserId]
    ON [BAR].[Visits]([UserId] ASC);

CREATE NONCLUSTERED INDEX [IX_Visits_WorkAreaId]
    ON [BAR].[Visits]([WorkAreaId]);

複数のユーザーがVisitItemsテーブルを次の方法で同時に更新したい場合:

別のWebリクエストにより、VisitItemsでVisitが作成されます(通常は1)。次に(問題のリクエスト):

  1. Webリクエストが来て、NHibernateセッションを開き、NHibernateトランザクションを開始します(READ_COMMITTED_SNAPSHOTをオンにしてRepeatable Readを使用)。
  2. VisitIdによる特定の訪問のすべての訪問アイテムを読み取ります。
  3. コードは、アイテムがまだ関連しているかどうか、または複雑なルールを使用して新しいアイテムが必要かどうかを評価します(40ミリ秒など、少し時間がかかります)。
  4. コードは、1つのアイテムを追加する必要があることを検出し、NHibernate Visit.VisitItems.Add(..)を使用して追加します
  5. コードは、1つのアイテム(追加したアイテムではなく)を削除する必要があることを識別し、NHibernate Visit.VisitItems.Remove(item)を使用して削除します。
  6. コードはトランザクションをコミットします

ツールを使用して、将来の実稼働環境で発生する可能性が高い12の同時要求をシミュレートします。

[編集]リクエストに応じて、ここに追加した調査の詳細の多くを削除して、短くしました。

多くの調査の後、次のステップは、where句で使用されるインデックス(つまり、削除に使用される主キー)とは異なるインデックスのヒントをロックする方法を考えることでした。そのため、ロックステートメントを:

var items = (List<VisitItem>)_session.CreateSQLQuery(@"SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
        WHERE VisitId = :visitId")
        .AddEntity(typeof(VisitItem))
        .SetParameter("visitId", qi.Visit.Id)
        .List<VisitItem>();

これにより、頻度のデッドロックはわずかに減少しましたが、依然として発生していました。そして、ここで私は迷子になり始めています:

3つの排他的ロック?

<deadlock-list>
  <deadlock victim="process3f71e64e8">
    <process-list>
      <process id="process3f71e64e8" taskpriority="0" logused="0" waitresource="KEY: 5:72057594071744512 (a5e1814e40ba)" waittime="3812" ownerId="8004520" transactionname="user_transaction" lasttranstarted="2015-12-14T10:24:58.010" XDES="0x3f7cb43b0" lockMode="X" schedulerid="1" kpid="15788" status="suspended" spid="63" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2015-12-14T10:24:58.013" lastbatchcompleted="2015-12-14T10:24:58.013" lastattention="1900-01-01T00:00:00.013" clientapp=".Net SqlClient Data Provider" hostname="ABC" hostpid="10016" loginname="bsapp" isolationlevel="repeatable read (3)" xactid="8004520" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
          <frame procname="adhoc" line="1" stmtstart="18" stmtend="254" sqlhandle="0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000">
            unknown
          </frame>
          <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
            unknown
          </frame>
        </executionStack>
        <inputbuf>
          (@p0 int)SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
          WHERE VisitId = @p0
        </inputbuf>
      </process>
      <process id="process4105af468" taskpriority="0" logused="1824" waitresource="KEY: 5:72057594071744512 (8194443284a0)" waittime="3792" ownerId="8004519" transactionname="user_transaction" lasttranstarted="2015-12-14T10:24:58.010" XDES="0x3f02ea3b0" lockMode="S" schedulerid="8" kpid="15116" status="suspended" spid="65" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-12-14T10:24:58.033" lastbatchcompleted="2015-12-14T10:24:58.033" lastattention="1900-01-01T00:00:00.033" clientapp=".Net SqlClient Data Provider" hostname="ABC" hostpid="10016" loginname="bsapp" isolationlevel="repeatable read (3)" xactid="8004519" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
          <frame procname="adhoc" line="1" stmtstart="18" stmtend="98" sqlhandle="0x0200000075abb0074bade5aa57b8357410941428df4d54130000000000000000000000000000000000000000">
            unknown
          </frame>
          <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">
            unknown
          </frame>
        </executionStack>
        <inputbuf>
          (@p0 int)DELETE FROM BAR.VisitItems WHERE Id = @p0
        </inputbuf>
      </process>
    </process-list>
    <resource-list>
      <keylock hobtid="72057594071744512" dbid="5" objectname="BAR.VisitItems" indexname="PK_BAR.VisitItems" id="lock449e27500" mode="X" associatedObjectId="72057594071744512">
        <owner-list>
          <owner id="process4105af468" mode="X"/>
        </owner-list>
        <waiter-list>
          <waiter id="process3f71e64e8" mode="X" requestType="wait"/>
        </waiter-list>
      </keylock>
      <keylock hobtid="72057594071744512" dbid="5" objectname="BAR.VisitItems" indexname="PK_BAR.VisitItems" id="lock46a525080" mode="X" associatedObjectId="72057594071744512">
        <owner-list>
          <owner id="process3f71e64e8" mode="X"/>
        </owner-list>
        <waiter-list>
          <waiter id="process4105af468" mode="S" requestType="wait"/>
        </waiter-list>
      </keylock>
    </resource-list>
  </deadlock>
</deadlock-list>

結果のクエリ数のトレースは次のようになります。
[編集]おっと。何週間ですか。これで、デッドロックにつながると思われる関連ステートメントの編集されていないトレースでトレースを更新しました。

exec sp_executesql N'SELECT * FROM BAR.VisitItems WITH (XLOCK, INDEX([PK_BAR.VisitItems]))
                WHERE VisitId = @p0',N'@p0 int',@p0=3826
go
exec sp_executesql N'SELECT visititems0_.VisitId as VisitId1_, visititems0_.Id as Id1_, visititems0_.Id as Id37_0_, visititems0_.VisitType as VisitType37_0_, visititems0_.FeeItemId as FeeItemId37_0_, visititems0_.FeeRateType as FeeRateT4_37_0_, visititems0_.Amount as Amount37_0_, visititems0_.GST as GST37_0_, visititems0_.Quantity as Quantity37_0_, visititems0_.Total as Total37_0_, visititems0_.ServiceFeeType as ServiceF9_37_0_, visititems0_.ServiceText as Service10_37_0_, visititems0_.InvoiceToCentre as Invoice11_37_0_, visititems0_.IsDefault as IsDefault37_0_, visititems0_.OverrideCode as Overrid13_37_0_, visititems0_.IsSurchargeItem as IsSurch14_37_0_, visititems0_.VisitId as VisitId37_0_, visititems0_.InvoicingProviderId as Invoici16_37_0_, visititems0_.SourceVisitItemId as SourceV17_37_0_ FROM BAR.VisitItems visititems0_ WHERE visititems0_.VisitId=@p0',N'@p0 int',@p0=3826
go
exec sp_executesql N'INSERT INTO BAR.VisitItems (VisitType, FeeItemId, FeeRateType, Amount, GST, Quantity, Total, ServiceFeeType, ServiceText, InvoiceToCentre, IsDefault, OverrideCode, IsSurchargeItem, VisitId, InvoicingProviderId, SourceVisitItemId) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15); select SCOPE_IDENTITY()',N'@p0 int,@p1 int,@p2 int,@p3 decimal(28,5),@p4 decimal(28,5),@p5 int,@p6 decimal(28,5),@p7 int,@p8 nvarchar(4000),@p9 bit,@p10 bit,@p11 int,@p12 bit,@p13 int,@p14 int,@p15 int',@p0=1,@p1=452,@p2=1,@p3=0,@p4=0,@p5=1,@p6=0,@p7=1,@p8=NULL,@p9=0,@p10=1,@p11=0,@p12=0,@p13=3826,@p14=3535,@p15=NULL
go
exec sp_executesql N'UPDATE BAR.Visits SET VisitType = @p0, DateOfService = @p1, InvoiceAnnotation = @p2, DefaultItemOverride = @p3, AppointmentId = @p4, ReferralRequired = @p5, ReferralCarePlan = @p6, UserId = @p7, PatientId = @p8, WorkAreaId = @p9, DidNotWaitAdjustmentId = @p10, ReferralId = @p11 WHERE Id = @p12',N'@p0 int,@p1 datetimeoffset(7),@p2 nvarchar(4000),@p3 bit,@p4 int,@p5 bit,@p6 nvarchar(4000),@p7 int,@p8 int,@p9 int,@p10 int,@p11 int,@p12 int',@p0=1,@p1='2016-01-22 12:37:06.8915296 +08:00',@p2=NULL,@p3=0,@p4=NULL,@p5=0,@p6=NULL,@p7=3535,@p8=4246,@p9=2741,@p10=NULL,@p11=NULL,@p12=3826
go
exec sp_executesql N'DELETE FROM BAR.VisitItems WHERE Id = @p0',N'@p0 int',@p0=7919
go

デッドロックグラフに表示されているため、私のロックは効果があるようです。でも何?3つの排他ロックと1つの共有ロック?同じオブジェクト/キーでどのように機能しますか?排他ロックを持っている限り、他の人から共有ロックを取得できないと思いましたか?そしてその逆。共有ロックを持っている場合、誰も排他ロックを取得できず、待機する必要があります。

ここでは、同じテーブルの複数のキーでロックが取得された場合のロックの仕組みについての理解が不足していると思います。

私が試したものとその影響のいくつかを以下に示します。

  • IX_Visit_Idの別のインデックスヒントをロックステートメントに追加しました。変化なし
  • IX_Visit_Idに2番目の列(VisitItem列のID)を追加しました。ずっと取って来たが、とにかく試した。変化なし
  • 分離レベルをコミット済み読み取りに変更しました(プロジェクトのデフォルト)。デッドロックはまだ発生しています。
  • 分離レベルをシリアル化可能に変更しました。デッドロックはまだ発生していますが、さらに悪いことです(異なるグラフ)。とにかく、私は本当にそれをしたくありません。
  • テーブルロックを取得すると、それらは(明らかに)なくなりますが、誰がそれをしたいでしょうか?
  • 悲観的なアプリケーションロック(sp_getapplockを使用)を取得することはできますが、それはテーブルロックとほとんど同じことです。
  • XLOCKヒントにREADPASTヒントを追加しても違いはありませんでした
  • インデックスとPKでPageLockをオフにしましたが、違いはありません
  • XLOCKヒントにROWLOCKヒントを追加しましたが、違いはありません。

NHibernateについての補足:使用方法と私はそれが機能することを理解しています そのため、ほとんどのステートメント(たとえば、遅延ロードされたVisitItemsのAggregateリスト=> Visit.VisitItems)は、必要な場合にのみ実行されます。トランザクションからの実際の更新および削除ステートメントのほとんどは、トランザクションがコミットされたときに最後に実行されます(上記のsqlトレースから明らかです)。実行順序を実際に制御することはできません。NHibernateは、いつ何をするかを決定します。私の最初のロックステートメントは、実際には回避策にすぎません。

また、lockステートメントでは、アイテムを未使用のリストに読み込んでいます(VisitオブジェクトのVisitItemsリストをオーバーライドしようとはしていません。NHibernateが私が知る限りでは動作するはずがないからです)。したがって、最初にカスタムステートメントでリストを読んだとしても、NHibernateは、それをどこかに遅延ロードするときにトレースで確認できる別のSQL呼び出しを使用して、プロキシオブジェクトコレクションVisit.VisitItemsにリストを再度ロードします。

しかし、それは問題ではないでしょう?私はすでに鍵をロックしていますか?再度ロードしてもそれは変わりませんか?

最後のメモとして、おそらく明確にするために、各プロセスは最初にVisitItemsで独自のVisitを追加し、次にそれを入力して変更します(削除および挿入とデッドロックをトリガーします)。私のテストでは、まったく同じVisitまたはVisitItemsを変更するプロセスはありません。

誰もこれにさらにアプローチする方法についてアイデアを持っていますか?私はこれをスマートな方法で回避しようとすることができますか(テーブルロックなどはありません)?また、同じオブジェクトでこのトリプルXロックが可能になる理由も知りたいと思います。分かりません。

パズルを解決するためにさらに情報が必要かどうかを教えてください。

[編集] 関係する2つのテーブルのDDLで質問を更新しました。

また、期待についての説明を求められました。はい、ここでいくつかのデッドロックがあります。しかし、12人の同時ユーザーがいる現在の頻度では、せいぜい数時間に1人しか存在しないと思います。現在、1分間に複数回ポップアップします。

それに加えて、trancount = 2に関するいくつかの情報を取得しました。これは、ネストされたトランザクションの問題を示している可能性がありますが、実際には使用していません。私もそれを調査し、結果をここに文書化します。


2
SELECT *は使用しないでください。それはあなたの問題の一因かもしれません。stackoverflow.com/questions/3639861/を
...

また、SELECT OBJECT_NAME(objectid, dbid) AS objectname, * FROM sys.dm_exec_sql_text(0x0200000024a9e43033ef90bb631938f939038627209baafb0000000000000000000000000000000000000000)各executionStackフレームでsqlhandleを実行して、実際に実行されているものをさらに判別します。
JamieSee

おそらくハッシュ衝突に直面していますか?dba.stackexchange.com/questions/80088/insert-only-deadlocks/...
Johnboy

皆さん、私はもうこのプロジェクトには参加していないのではないかと思います:-/ですから、あなたの提案を試すことはできません。ただし、スレッドとすべての情報を一部のチームメンバに転送して、代わりに調査できるようにしました。
ベン

この質問に対するPowerShellスクリプトの回答を使用して、役立つデッドロックの詳細を取得できます。具体的には、「不明な」スタックフレームのSQLステートメント情報を取得します。dba.stackexchange.com/questions/28996/…– JamieSee
22:42

回答:


2

この効果についていくつかコメントしましたが、Repeatable ReadトランザクションアイソレーションレベルとRead Committed Snapshotを組み合わせたときに、目的の結果が得られるかどうかはわかりません。

デッドロックリストで報告されるTILは反復可能読み取りであり、Read Committedよりもさらに制限が厳しく、記述したフローを考えると、デッドロックにつながる可能性があります。

しようとしているのは、DB TILを繰り返し読み取り可能のままにして、トランザクション分離レベルの設定ステートメントでスナップショットTILを明示的に使用するようにトランザクションを設定することです。参照:https : //msdn.microsoft.com/en-us/library/ms173763.aspx もしそうなら、何か間違っている必要があると思います。私はnHibernateに精通していませんが、ここに参照があるようです:http ://www.anujvarma.com/fluent-nhibernate-setting-database-transaction-isolation-level/

アプリのアーキテクチャで許可されている場合は、コミットされたスナップショットをdbレベルで読み取り、まだデッドロックが発生する場合は、行のバージョン管理でスナップショットを有効にするオプションがあります。これを行う場合、スナップショット(行のバージョン管理)を有効にする場合は、tempdbセットアップを再考する必要があることに注意してください。あなたがそれを必要とするならば、私はあなたにこれに関するあらゆる種類の材料を手に入れることができます-私に知らせてください。


2

私はいくつかの考えがあります。まず、デッドロックを回避する最も簡単な方法は、常に同じ順序でロックを取得することです。つまり、明示的なトランザクションを使用する異なるコードは同じ順序でオブジェクトにアクセスする必要がありますが、明示的なトランザクションのキーによって行に個別にアクセスする場合は、そのキーでソートする必要があります。実行Visit.VisitItemsする前にPKで並べ替えるAddDelete、これが巨大なコレクションである場合を除き、並べ替えてみてくださいSELECT

ただし、ソートはおそらくここでの問題ではありません。2つのスレッドがすべてのVisitItemIDsで共有ロックを取得しVisitID、スレッドA DELETEが完了できないのは、スレッドBが共有ロックを解放するまでDELETE完了しないためです。ここではアプリロックが機能しますが、テーブルロックほど悪くはありませんSELECT。メソッドごとにブロックするだけで、他のロックはうまく機能するからです。またVisit、指定されたテーブルの排他ロックを取得することもできますがVisitID、これは潜在的に過剰です。

ハードデリートを(UPDATE ... SET IsDeleted = 1使用ではなくDELETE)ソフトデリートに変更し、これらのレコードを後で、明示的なトランザクションを使用しないクリーンアップジョブを使用して一括でクリーニングすることをお勧めします。これは明らかに、これらの削除された行を無視するために他のコードをリファクタリングする必要がありますが、明示的なトランザクションにDELETE含まれるs を処理するための私の好ましい方法ですSELECT

またSELECT、トランザクションからを削除して、楽観的な同時実行モデルに切り替えることもできます。Entity Frameworkはこれを無料で行いますが、NHibernateについてはわかりません。EFがDELETE影響する0行を返す場合、楽観的同時実行例外が発生します。


1

visitItemsを変更する前に、Visitsアップデートを移動しようとしましたか?そのXロックは「子」行を保護する必要があります。

完全なロックを取得したトレース(および人間が読める形式への変換)を行うことは多くの作業ですが、シーケンスがより明確に表示される場合があります。



-1

READ COMMITTED SNAPSHOT ONは、READ COMMITTED ISOLATION LEVELで実行されるすべての単一トランザクションがREAD COMMITTED SNAPSHOTとして機能することを意味します。

つまり、リーダーはライターをブロックせず、ライターはリーダーをブロックしません。

反復可能な読み取りトランザクション分離レベルを使用しているため、デッドロックが発生します。Read Committed(スナップショットなし)は、ステートメント終了まで行/ページのロックを保持しますが、反復可能読み取りはトランザクションの終了までロックを保持します。

デッドロックグラフを見ると、取得された「S」ロックを確認できます。これは2番目のポイントによるロックだと思います->「VisitIdで特定の訪問のすべての訪問アイテムを読み取ります。」

  1. NHibernate接続のトランザクション分離レベルをRead Committedに変更します
  2. 2番目のポイントのクエリを分析し、visitID列にインデックスがある場合にPKでロックを取得する理由を理解する必要があります(インデックスに含まれる列が欠落しているためかもしれません)。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.