この問題についての私の見解は次のとおりです。
-
複数のスレッドを使用してSQLServerまたは任意のデータベースにデータを挿入/更新/クエリする場合、デッドロックは現実のものです。それらが発生すると想定し、適切に処理する必要があります。
-
デッドロックの発生を制限しようとすべきではないというわけではありません。ただし、デッドロックの基本的な原因を理解し、それらを防ぐための手順を実行するのは簡単ですが、SQLServerは常にあなたを驚かせます:-)
デッドロックの理由:
-
スレッドが多すぎます-スレッドの数を最小限に抑えてください。ただし、もちろん、最大のパフォーマンスを得るには、より多くのスレッドが必要です。
-
インデックスが足りません。選択と更新が十分に選択的でない場合、SQLは正常な範囲よりも広い範囲のロックを取得します。適切なインデックスを指定してみてください。
-
インデックスが多すぎます。インデックスを更新するとデッドロックが発生するため、インデックスを必要最小限に減らすようにしてください。
-
トランザクションの分離レベルが高すぎます。 .NETを使用する場合のデフォルトの分離レベルは「Serializable」ですが、SQLServerを使用する場合のデフォルトの分離レベルは「ReadCommitted」です。分離レベルを下げると、非常に役立ちます(もちろん適切な場合)。
これが私があなたの問題に取り組む方法です:
-
独自のスレッドソリューションを作成するのではなく、TaskParallelライブラリを使用します。私の主な方法は次のようになります:
using (var dc = new TestDataContext()) { // Get all the ids of interest. // I assume you mark successfully updated rows in some way // in the update transaction. List<int> ids = dc.TestItems.Where(...).Select(item => item.Id).ToList(); var problematicIds = new List<ErrorType>(); // Either allow the TaskParallel library to select what it considers // as the optimum degree of parallelism by omitting the // ParallelOptions parameter, or specify what you want. Parallel.ForEach(ids, new ParallelOptions {MaxDegreeOfParallelism = 8}, id => CalculateDetails(id, problematicIds)); }
-
デッドロックの失敗を再試行してCalculateDetailsメソッドを実行します
private static void CalculateDetails(int id, List<ErrorType> problematicIds) { try { // Handle deadlocks DeadlockRetryHelper.Execute(() => CalculateDetails(id)); } catch (Exception e) { // Too many deadlock retries (or other exception). // Record so we can diagnose problem or retry later problematicIds.Add(new ErrorType(id, e)); } }
-
コアのCalculateDetailsメソッド
private static void CalculateDetails(int id) { // Creating a new DeviceContext is not expensive. // No need to create outside of this method. using (var dc = new TestDataContext()) { // TODO: adjust IsolationLevel to minimize deadlocks // If you don't need to change the isolation level // then you can remove the TransactionScope altogether using (var scope = new TransactionScope( TransactionScopeOption.Required, new TransactionOptions {IsolationLevel = IsolationLevel.Serializable})) { TestItem item = dc.TestItems.Single(i => i.Id == id); // work done here dc.SubmitChanges(); scope.Complete(); } } }
-
そしてもちろん、デッドロック再試行ヘルパーの実装
public static class DeadlockRetryHelper { private const int MaxRetries = 4; private const int SqlDeadlock = 1205; public static void Execute(Action action, int maxRetries = MaxRetries) { if (HasAmbientTransaction()) { // Deadlock blows out containing transaction // so no point retrying if already in tx. action(); } int retries = 0; while (retries < maxRetries) { try { action(); return; } catch (Exception e) { if (IsSqlDeadlock(e)) { retries++; // Delay subsequent retries - not sure if this helps or not Thread.Sleep(100 * retries); } else { throw; } } } action(); } private static bool HasAmbientTransaction() { return Transaction.Current != null; } private static bool IsSqlDeadlock(Exception exception) { if (exception == null) { return false; } var sqlException = exception as SqlException; if (sqlException != null && sqlException.Number == SqlDeadlock) { return true; } if (exception.InnerException != null) { return IsSqlDeadlock(exception.InnerException); } return false; } }
-
もう1つの可能性は、パーティショニング戦略を使用することです
テーブルを自然に複数の異なるデータセットに分割できる場合は、SQL Serverのパーティション化されたテーブルとインデックスを使用するか、既存のテーブルを手動で複数のテーブルセットに分割することができます。 2番目のオプションは面倒なので、SQLServerのパーティショニングを使用することをお勧めします。また、組み込みのパーティショニングはSQLEnterpriseEditionでのみ使用できます。
パーティショニングが可能な場合は、データを分割するパーティショニングスキームを選択できます。たとえば、8つの異なるセットを選択できます。これで、元のシングルスレッドコードを使用できますが、それぞれが個別のパーティションを対象とする8つのスレッドがあります。これで、デッドロックは発生しなくなります(または少なくとも最小数)。
それが理にかなっていることを願っています。