Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

Wielowątkowa aplikacja C# z wywołaniami bazy danych SQL Server

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.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQL Pivot z wieloma kolumnami

  2. Kopiuj tabele z jednej bazy danych do drugiej w SQL Server

  3. Pobierz dzień roku z daty w SQL Server (T-SQL)

  4. SQL Server Typ tekstowy a typ danych varchar

  5. Jak uzyskać kalendarz kwartał z daty w TSQL?