Oto moje podejście do problemu:
-
W przypadku korzystania z wielu wątków do wstawiania/aktualizowania/odpytywania danych w programie SQL Server lub dowolnej bazie danych zakleszczenia są faktem. Musisz założyć, że wystąpią i odpowiednio się nimi zająć.
-
Nie znaczy to, że nie powinniśmy próbować ograniczać występowania impasów. Jednak łatwo jest poznać podstawowe przyczyny zakleszczeń i podjąć kroki, aby im zapobiec, ale SQL Server zawsze Cię zaskoczy :-)
Jakiś powód impasu:
-
Za dużo wątków - spróbuj ograniczyć liczbę wątków do minimum, ale oczywiście chcemy mieć więcej wątków dla maksymalnej wydajności.
-
Za mało indeksów. Jeśli selekcje i aktualizacje nie są wystarczająco selektywne, SQL usunie większe blokady zakresu niż jest to zdrowe. Spróbuj określić odpowiednie indeksy.
-
Za dużo indeksów. Aktualizacja indeksów powoduje zakleszczenia, więc spróbuj zredukować indeksy do wymaganego minimum.
-
Zbyt wysoki poziom izolacji transakcji. Domyślny poziom izolacji podczas korzystania z platformy .NET to "Serializable", podczas gdy domyślnym użyciem programu SQL Server jest "Read Committed". Zmniejszenie poziomu izolacji może bardzo pomóc (oczywiście w razie potrzeby).
Oto jak mogę rozwiązać Twój problem:
-
Nie wypuściłbym własnego rozwiązania do obsługi wątków, skorzystałbym z biblioteki TaskParallel. Moja główna metoda wyglądałaby mniej więcej tak:
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)); }
-
Wykonaj metodę CalculateDetails z ponawianiem prób w przypadku niepowodzeń zakleszczenia
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)); } }
-
Podstawowa metoda 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(); } } }
-
I oczywiście moja implementacja pomocnika ponawiania zakleszczenia
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; } }
-
Kolejną możliwością jest użycie strategii partycjonowania
Jeśli tabele można w naturalny sposób podzielić na kilka odrębnych zestawów danych, można albo użyć partycjonowanych tabel i indeksów programu SQL Server, albo ręcznie podzielić istniejące tabele na kilka zestawów tabel. Sugerowałbym użycie partycjonowania SQL Server, ponieważ druga opcja byłaby nieporządna. Również wbudowane partycjonowanie jest dostępne tylko w SQL Enterprise Edition.
Jeśli partycjonowanie jest dla Ciebie możliwe, możesz wybrać schemat partycjonowania, który zepsuł dane w powiedzmy 8 odrębnych zestawach. Teraz możesz użyć oryginalnego kodu jednowątkowego, ale mieć 8 wątków, z których każdy jest skierowany na oddzielną partycję. Teraz nie będzie żadnych (lub przynajmniej minimalnej liczby) zakleszczeń.
Mam nadzieję, że to ma sens.