私はこのデッドロック問題にかなりの数日間取り組んできましたが、私が何をしても、それは何らかの形で持続します。
まず、一般的な前提:VisitItemsを使用した訪問は1対多の関係にあります。
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(通常は1)でVisitが作成されます。次に(問題のリクエスト):
ツールを使用して、将来の実稼働環境で発生する可能性が高い12の同時要求をシミュレートします。
[EDIT]リクエストに応じて、ここに追加した調査の詳細の多くを削除して、短くしました。
多くの調査の後、次のステップは、where句で使用されているものとは異なるインデックス(つまり、削除に使用されているため、主キー)とは異なるインデックスのヒントをロックする方法を考えることでしたので、lockステートメントを次のように変更しました:
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>();
これにより、デッドロックの頻度はわずかに減少しましたが、依然として発生していました。そして、ここで私は迷子になり始めています。
<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つの共有ロック?同じオブジェクト/キーでどのように機能しますか?あなたが排他ロックを持っている限り、誰かから共有ロックを取得することはできないと思いましたか?そしてその逆。あなたが共有ロックを持っている場合、誰も排他ロックを取得することができません、彼らは待つ必要があります。
同じテーブルの複数のキーに対してロックが実行された場合のロックの仕組みについて、ここでの深い理解が不足していると思います。
ここに私が試したことのいくつかとその影響があります:
NHibernateに関するいくつかの補足:それが使用され、私がそれが機能することを理解している方法は、フラッシュを呼び出しない限り、SQLステートメントを実際に実行する必要があるまでキャッシュすることです。したがって、ほとんどのステートメント(VisitItems => Visit.VisitItemsの遅延ロードされたAggregateリストなど)は、必要な場合にのみ実行されます。トランザクションからの実際の更新および削除ステートメントのほとんどは、トランザクションがコミットされたときに最後に実行されます(上記のSQLトレースから明らかです)。私は実際には実行順序を制御できません。 NHibernateは、いつ何をするかを決定します。私の最初のロックステートメントは、実際には回避策にすぎません。
また、lockステートメントを使用して、アイテムを未使用のリストに読み取っているだけです(VisiveオブジェクトのVisitItemsリストをオーバーライドしようとしているわけではありません。これは、NHibernateが私の知る限りでは機能しないためです)。したがって、カスタムステートメントを使用して最初にリストを読み取ったとしても、NHibernateは、別のSQL呼び出しを使用して、プロキシオブジェクトコレクションVisit.VisitItemsにリストを再度ロードし、どこかに遅延ロードするときにトレースで確認できます。
しかし、それは重要ではないでしょう?上記の鍵はすでにロックされていますか?それを再度ロードしてもそれは変わりませんか?
最後に、明確にするために:各プロセスは、最初にVisitItemsを使用して独自のVisitを追加してから、それを変更します(削除と挿入およびデッドロックをトリガーします)。私のテストでは、まったく同じVisitまたはVisitItemsを変更するプロセスはありません。
これにこれ以上アプローチする方法について誰かがアイデアを持っていますか?スマートな方法でこれを回避するために試すことができるものはありますか(テーブルロックなどなし)?また、なぜこのトリプルXロックが同じオブジェクトに対しても可能であるのかを知りたいのです。わかりません。
パズルを解くためにさらに情報が必要な場合はお知らせください。
[EDIT]関係する2つのテーブルのDDLで質問を更新しました。
また、私は期待についての説明を求められました:はい、ここにいくつかのデッドロックがありますが、OKです。再試行するか、ユーザーに再送信してもらいます(一般的に言えば)。しかし、12人の同時ユーザーがいる現在の頻度では、せいぜい数時間ごとに1人しかいないと思います。現在、1分間に複数回ポップアップします。
それに加えて、私はtrancount = 2に関するいくつかの詳細情報を取得しました。これは、ネストされたトランザクションの問題を示している可能性があり、実際には使用していません。私もそれを調査し、結果をここに文書化します。
私はいくつかの考えを持っています。まず、デッドロックを回避する最も簡単な方法は、常に同じ順序でロックを取得することです。つまり、明示的なトランザクションを使用する異なるコードは同じ順序でオブジェクトにアクセスする必要がありますが、明示的なトランザクションでキーによって行に個別にアクセスする場合も、そのキーでソートする必要があります。並べ替えてみてくださいVisit.VisitItems
Add
またはDelete
を実行する前にPKを使用してください。ただし、これが巨大なコレクションである場合を除き、SELECT
でソートします。
ここでのソートはおそらくあなたの問題ではありません。 2つのスレッドが特定のVisitItemID
のすべてのVisitID
sの共有ロックを取得し、スレッドAのDELETE
は、スレッドBが獲得した共有ロックを解放するまで完了できないと思いますDELETE
が完了するまでは。アプリロックはここで機能し、メソッドでブロックするだけで他のSELECT
sは問題なく機能するため、テーブルロックほど悪くありません。また、指定されたVisit
のVisitID
テーブルに排他ロックをかけることもできますが、これも過剰になる可能性があります。
完全削除を一時削除に変更することをお勧めします(UPDATE ... SET IsDeleted = 1
DELETE
)を使用する代わりに、これらのレコードを後で明示的にトランザクションを使用しないクリーンアップジョブを使用して一括でクリーニングします。これは明らかにこれらの削除された行を無視するために他のコードをリファクタリングする必要がありますが、明示的なトランザクションでDELETE
に含まれるSELECT
sを処理するための私の好ましい方法です。
トランザクションからSELECT
を削除して、楽観的同時実行モデルに切り替えることもできます。エンティティフレームワークはこれを無料で行いますが、NHibernateについては不明です。 DELETE
が影響を受ける0行を返す場合、EFは楽観的同時実行例外を発生させます。
この影響についていくつかコメントしましたが、Repeatable Readトランザクション分離レベルとRead Committedスナップショットを組み合わせると、期待どおりの結果が得られるかわかりません。
デッドロックリストで報告された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の設定を再考する必要があることに注意してください。必要に応じて、あらゆる種類の資料を入手できます。お知らせください。
VisitItemsを変更する前に、Visitsの更新を移動してみましたか?そのxロックは「子」行を保護する必要があります。
取得したトレースを完全にロック(および人間が読める形式に変換)することは多くの作業ですが、シーケンスをより明確に表示する場合があります。