EFコードの最初のモデルを介して更新しているデータベースがあります。このデータベースには、1 つの「親」テーブルと 10 または 12 の「子」テーブルが含まれています。親テーブルと子テーブルの間に外部キー関係があります。
最初に、子テーブルにレコードを作成する場合、EF を使用して親行を作成し、主キーを取得し、EF を使用して子行を作成し、主キーを子の外部キーとして使用します。 .
最近、TPT 継承を使用するようにモデルをリファクタリングしました。これで、基本クラス (基本テーブルに対応) と子クラス (子テーブルに対応) ができました。したがって、子レコードを作成する場合は、子タイプのオブジェクトを作成し、すべての必須フィールドに入力して、コンテキストを使用して保存します。EF が生成する SQL は、私が手動で行っていたのとほぼ同じことを行うと想定しています (論理的であるため)。
- 親レコードを作成します。
- 新しい親レコードの主キーを取得
- 子レコードを作成
問題は、TPT 継承を使用すると、デッドロック エラーが発生することです。このデータベースにエントリを作成しようとすると、1 日に 5 ~ 10 回、デッドロック エラーが発生します。このデータベースは Web サイトのログ データベースなので、操作の 90% は書き込みです。サイトでの活動を記録するために使用します。
Web サイトのテスト インスタンスに十分な負荷をかけると、エラーを頻繁に発生させることが簡単にできます。私は DBA から SQL トレースを取得しようとしていますが、これを自分で行う能力がないためです (テスト中であっても)。それが最も役立つことはわかっており、それが得られたらここに投稿しますが、最初の質問は、私が間違っていることは明らかですか? 私はこれをオンラインで調査し、EF が使用する同時実行の種類を見つけようとしましたが、回答はまちまちでした。SQL サーバーのデフォルト (コミットされた読み取り) を使用すると言う人もいれば、TransactionScope オブジェクトでクエリをラップするため、Serializable であるデフォルトの TransactionScope 分離レベルを使用すると言う人もいます。
TPT継承ツリーの一部であるエンティティを作成するときに、どの分離レベルが使用されているか、および/またはどのSQLが生成されるかを誰が確実に言うことができますか?
ありがとう。
編集 元の投稿のコメントからの@usrの提案に応えて、このようにReadCommittedレベルを使用して、保存をトランザクションスコープにラップしようとしました
TransactionOptions options = new TransactionOptions();
options.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, options))
{
retVal = AuditEventContext.SaveChanges();
scope.Complete();
}
それでも同じデッドロック エラーが発生します。DBA から SQL トレースを取得中です。
このアプリケーションにさらにコンテキストを追加するために、コンテキスト (上記のコードの AuditEventContext) が MVC アプリケーションによって使用されていることを追加します。各コントローラーは、AuditEventContext の独自のインスタンスを取得するため、同じデータベースを更新する複数のスレッドに複数のコンテキストがあります。それがデッドロックの原因であると確信しています。特に、元のコードが継承ベースの挿入に対して EF が行っていると想定していることを模倣していたため、その理由はよくわかりません。
以下の質問に答えて#2を編集
このデータを読み取るアプリケーションは他にもありますが、何も読み取っていないことがわかっている場合は、このエラーを発生させることができます。一度に 50 人ほどのユーザーを自分の Web サイトに放り込むと、これはほとんどすぐに発生します。幸いなことに、サイトに同時に 50 人のユーザーがいるわけではありません :) 多くの行を挿入していません。これは監査データベースであるため、サイト全体に「監査イベント」を書き込みます。つまり、ページ リクエスト、法的条件の受け入れ、ログオフなどです。したがって、挿入は非常に小さくなります。コンテキストはコントローラーの存続期間中存在するため、同じコンテキストを使用して複数のイベントを挿入できる可能性がありますが、各トランザクションは小規模です。親テーブルのキーは自動生成された整数 ID であるため、重複キーを生成しないことを願っています。それを管理するのは SQL サーバーの仕事です。親テーブルには、ALLOW_ROW_LOCKS=ON および ALLOW_PAGE_LOCKS=ON の 4 つのインデックスがあります。これらは設定せず、SQL のデフォルトのままにしました。インデックスには IGNORE_DUP_KEY=OFF もあります
@RuneG、コンテキストは Unity によって生成されます (依存性注入)。私は直接それを作成していません。Dispose を呼び出していますが、using ステートメントでコンテキストを作成する場合のように、save changes を呼び出した直後ではなく、コンテキストを所有するコントローラーが破棄されたときに Dispose を呼び出しています。これが問題を引き起こしている可能性はありますか? 複数の更新に同じコンテキストを使用していますか?
編集 #3 - SQL のデッドロック
最後にSQLトレースを取得しました。SQL のデッドロックの例を次に示します。
exec sp_executesql N'insert [dbo].[PFIAccessLog]([AuditEventID], [Screen], [PolicyNumber], [CoverageSequence], [PersonTypeID])
values (@0, @1, @2, @3, @4)
select [PFIAccessLogID]
from [dbo].[PFIAccessLog]
where @@ROWCOUNT > 0 and [AuditEventID] = @0',N'@0 bigint,@1 nvarchar(100),@2 nvarchar(50),@3 nvarchar(50),@4 int',@0=2035631,@1=N'ContractList',@2=N'220001197',@3=N'1',@4=1
と
exec sp_executesql N'insert [dbo].[PFIAccessLog]([AuditEventID], [Screen], [PolicyNumber], [CoverageSequence], [PersonTypeID])
values (@0, @1, @2, @3, @4)
select [PFIAccessLogID]
from [dbo].[PFIAccessLog]
where @@ROWCOUNT > 0 and [AuditEventID] = @0',N'@0 bigint,@1 nvarchar(100),@2 nvarchar(50),@3 nvarchar(50),@4 int',@0=2035630,@1=N'ContractList',@2=N'220001197',@3=N'1',@4=1
PFIAccessLog は、TPT 階層の子テーブルの 1 つです。
編集 4
さて、私のコードは次のようになりましたが、まだデッドロックが発生しています:
TransactionOptions options = new TransactionOptions();
options.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, options))
{
context.Database.Connection.Open();
context.Database.Connection.EnlistTransaction(Transaction.Current);
retVal = context.SaveChanges();
scope.Complete();
}
また、MVC コントローラーの存続期間中 (複数の書き込みの可能性がある) コンテキストを共有するのではなく、書き込むたびに新しいコンテキストを作成するようにコードを変更しました。
編集#5
OK、@RuneGの提案で、分離レベルをコミットされていない読み取りに設定し、問題を修正しました。とはいえ、これには少し不安があります。デッドロックしているステートメントの例を見ると、私が読んでいるものの一部は主キーの値です。別のトランザクションからコミットされていないデータを読み取ることができ、これらのキーがデータベースによって自動生成される場合、読み返すときに他のレコード ID を取得できるのではないでしょうか?
これらの SQL スニペットのそれぞれがトランザクションで実行されていると仮定すると、それらは両方とも同じテーブルに対して 1 回の書き込みと 1 回の読み取りであるため、デッドロックは次のように発生すると想定しています。
トランザクション 1 はそのデータを書き込みます。排他ロックを取得および解放します。
トランザクション 1 は、読み取り用の共有ロックを取得します。トランザクション 2 は、その挿入のために書き込みロックを取得します。
バム、コミットされた読み取り中のデッドロックですよね?書き込みロックがあるため、トランザクション 1 を続行できません。トランザクション 2 がデータを書き込むのを防ぐ共有ロックがあるため、トランザクション 2 は続行できません。トランザクション 1 はダーティと見なす可能性があります。
だから私は今コミットされていない状態で読んでいると仮定します。
トランザクション 1 はそのデータを書き込みます。トランザクション 1 はデータを読み取ります トランザクション 2 はデータを書き込みます トランザクション 2 はデータを読み取ります。
プロセスの 1 つは排他ロックで他のプロセスをブロックできますが、共有ロックは取得されません。したがって、私の懸念は、主キーの生成がこれらすべてにどのように適合するかです。テーブルに排他ロックがある間に生成されると想定しているので、キーが「混同」されるリスクはありませんか? 他のプロセスがダーティ データを読み取り、最終的にロールバックされる可能性があるかどうかは気にしません。キーは私の関心事です。
さらに、これは複数のスレッドが同じ種類の SQL ステートメントを実行した結果です。スレッドは、私の MVC アプリのリクエストです。これは標準的なシナリオのようです。コードをスレッド セーフにする (つまり、一度にコード内のスレッドを 1 つだけにする) 場合、コミットされた読み取りを使用できると思いますか?
冗長で申し訳ありません。私は問題に取り組んでいるときにそうなる傾向があります:)