web-dev-qa-db-ja.com

注入されたDbContextで並列非同期呼び出しを使用するためのEF Coreのベストプラクティスは何ですか?

EF Core 1.1で.NET Core 1.1 APIを使用し、MicrosoftのVanillaセットアップを使用して、Dependency Injectionを使用してサービスにDbContextを提供しています。 (参照: https://docs.Microsoft.com/en-us/aspnet/core/data/ef-mvc/intro#register-the-context-with-dependency-injection

現在、 WhenAll を使用して最適化としてデータベース読み取りの並列化を検討しています

代わりに:

var result1 = await _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId);
var result2 = await _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp); 

私が使う:

var repositoryTask1 = _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId);     
var repositoryTask2 = _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp);   
(var result1, var result2) = await (repositoryTask1, repositoryTask2 ).WhenAll();

これらのDBリポジトリアクセスクラス以外で同じ戦略を使用し、複数のサービスにわたってコントローラーのWhenAllでこれらの同じメソッドを呼び出すまで、これはすべてうまくいきます。

var serviceTask1 = _service1.GetSomethingsFromDb(Id);
var serviceTask2 = _service2.GetSomeMoreThingsFromDb(Id);
(var dataForController1, var dataForController2) = await (serviceTask1, serviceTask2).WhenAll();

これをコントローラーから呼び出すと、ランダムに次のような同時実行エラーが発生します。

System.InvalidOperationException:ExecuteReaderには、オープンで使用可能な接続が必要です。接続の現在の状態は閉じられています。

私が信じる理由は、これらのスレッドが同じテーブルに同時にアクセスしようとすることがあるためです。 これはEF Coreの仕様によるものであることがわかっています 必要に応じて毎回新しいdbContextを作成できますが、回避策があるかどうかを確認しようとしています。それは、Mehdi El Gueddariによるこの良い投稿を見つけたときです: http://mehdi.me/ambient-dbcontext-in-ef6/

彼はこの制限を認めています:

挿入されたDbContextにより、サービスにマルチスレッドまたはあらゆる種類の並列実行フローを導入できなくなります。

DbContextScopeを使用したカスタム回避策を提供します。

ただし、彼はDbContextScopeを使用しても並行して動作しないという点で注意を示しています(上記のことを試みています)。

dbContextScopeのコンテキスト内で複数の並列タスクを開始しようとすると(たとえば、複数のスレッドまたは複数のTPLタスクを作成することにより)、大きな問題が発生します。これは、アンビエントDbContextScopeが、並列タスクが使用しているすべてのスレッドを通過するためです。

ここで彼の最後のポイントは私の質問に私を導きます:

一般に、単一のビジネストランザクション内でデータベースアクセスを並列化してもメリットはほとんどないか、大幅に複雑になります。ビジネストランザクションのコンテキスト内で実行される並列操作は、データベースにアクセスしないでください。

この場合、コントローラーでWhenAllを使用せず、1つずつ待機を使用する必要がありますか?または、ここでDbContextの依存性注入がより基本的な問題であるため、代わりに何らかの種類のファクトリーによって毎回新しいものを作成/提供する必要がありますか?

20
starmandeluxe

議論に答える唯一の方法は、パフォーマンス/負荷テストを行って同等の経験的統計的証拠を得ることが唯一の方法であるため、これを一度解決することができました。

私がテストしたものは次のとおりです。

標準のAzure webappで、VSTS @ 200ユーザー、最大4分間のクラウドロードテスト。

テスト#1:DbContextの依存性注入と各サービスのasync/awaitを使用した1つのAPI呼び出し。

テスト#1:の結果 enter image description here

テスト#2:各サービスメソッド呼び出し内でDbContextを新規作成し、WhenAllで並列スレッド実行を使用する1つのAPI呼び出し。

テスト#2の結果: enter image description here

結論:

結果を疑う人のために、さまざまなユーザー負荷でこれらのテストを数回実行しました。平均は基本的に毎回同じでした。

私の意見では、並列処理によるパフォーマンスの向上は取るに足りないものであり、これは、開発のオーバーヘッド/保守の負債、誤った取り扱いによるバグの可能性、Microsoftの公式勧告からの逸脱を引き起こす依存性注入を放棄する必要性を正当化するものではありません。

もう1つ注意してください:ご覧のように、実際にはWhenAll戦略で失敗したリクエストがいくつかありました。毎回新しいコンテキストが作成されることを保証する場合でも 。この理由はわかりませんが、10ミリ秒のパフォーマンスの向上よりも500エラーのほうがずっと好きです。

20
starmandeluxe

context.XyzAsync()メソッドを使用するのは、await呼び出されたメソッドを呼び出すか、contextスコープ。

DbContextインスタンスはスレッドセーフではありません。並列スレッドで使用しないでください。つまり、確かに、たとえ並列に実行していなくても、複数のスレッドで使用しないでください。それを回避しようとしないでください。

何らかの理由で並列データベース操作を実行したい場合(そしてできると思う デッドロック、同時実行の競合などを回避する )、それぞれが独自のDbContextインスタンスを持っていることを確認してください。ただし、並列化は主にCPUにバインドされたプロセスに役立ち、データベースの相互作用のようなIOにバインドされたプロセスには役立ちません。並列独立read操作の恩恵を受けることができるかもしれませんが、並列writeプロセス。デッドロックなどは別として、1つのトランザクションですべての操作を実行するのがはるかに難しくなります。

ASP.Netコアでは、通常、リクエストごとのコンテキストパターン(ServiceLifetime.Scopedhere を参照してください。ただし、それでもコンテキストを複数のスレッドに転送することはできません。結局、それを防ぐことができるのはプログラマーだけです。

常に新しいコンテキストを作成するパフォーマンスコストが心配な場合は、しないでください。基礎となるモデル(ストアモデル、概念モデル+それらの間のマッピング)が一度作成され、アプリケーションドメインに格納されるため、コンテキストの作成は軽量の操作です。また、新しいコンテキストはデータベースへの物理的な接続を作成しません。すべてのASP.Netデータベース操作は、物理接続のプールを管理する接続プールを介して実行されます。

これらすべてが、ベストプラクティスに合わせてDIを再構成する必要があることを意味する場合は、そのようにしてください。現在の設定がコンテキストを複数のスレッドに渡す場合、過去には設計上の決定が不十分でした。回避策によって避けられないリファクタリングを延期する誘惑に抵抗します。唯一の回避策は、コードの並列化を解除することです。したがって、最終的には、DIとコードを再設計する場合よりも遅くなる可能性があります。スレッドごとのコンテキストに。

18
Gert Arnold