Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Multithreading-C#-Anwendung mit SQL Server-Datenbankaufrufen

Hier ist meine Lösung des Problems:

  • Wenn mehrere Threads zum Einfügen/Aktualisieren/Abfragen von Daten in SQL Server oder einer beliebigen Datenbank verwendet werden, sind Deadlocks eine Tatsache des Lebens. Sie müssen davon ausgehen, dass sie auftreten, und entsprechend damit umgehen.

  • Das bedeutet nicht, dass wir nicht versuchen sollten, das Auftreten von Deadlocks zu begrenzen. Es ist jedoch einfach, sich über die grundlegenden Ursachen von Deadlocks zu informieren und Maßnahmen zu ergreifen, um sie zu verhindern, aber SQL Server wird Sie immer wieder überraschen :-)

Einige Gründe für Deadlocks:

  • Zu viele Threads - versuchen Sie, die Anzahl der Threads auf ein Minimum zu beschränken, aber natürlich wollen wir mehr Threads für maximale Leistung.

  • Nicht genügend Indizes. Wenn Auswahlen und Aktualisierungen nicht selektiv genug sind, entfernt SQL größere Bereichssperren, als gesund sind. Versuchen Sie, geeignete Indizes anzugeben.

  • Zu viele Indizes. Das Aktualisieren von Indizes verursacht Deadlocks, versuchen Sie also, die Indizes auf das erforderliche Minimum zu reduzieren.

  • Transaktionsisolationsstufe zu hoch. Die Standard-Isolationsstufe bei Verwendung von .NET ist „Serializable“, während die Standardeinstellung bei Verwendung von SQL Server „Read Committed“ ist. Das Reduzieren des Isolationslevels kann sehr hilfreich sein (wenn es natürlich angebracht ist).

So könnte ich Ihr Problem angehen:

  • Ich würde keine eigene Threading-Lösung entwickeln, ich würde die TaskParallel-Bibliothek verwenden. Meine Hauptmethode würde in etwa so aussehen:

    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));
    }
    
  • Führen Sie die CalculateDetails-Methode mit Wiederholungen für Deadlock-Fehler aus

    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));
        }
    }
    
  • Die CalculateDetails-Kernmethode

    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();
            }
        }
    }
    
  • Und natürlich meine Implementierung eines Deadlock-Wiederholungshelfers

    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;
        }
    }
    
  • Eine weitere Möglichkeit ist die Verwendung einer Partitionierungsstrategie

Wenn Ihre Tabellen natürlich in mehrere unterschiedliche Datensätze partitioniert werden können, können Sie entweder partitionierte SQL Server-Tabellen und -Indizes verwenden oder Ihre vorhandenen Tabellen manuell in mehrere Tabellensätze aufteilen. Ich würde empfehlen, die Partitionierung von SQL Server zu verwenden, da die zweite Option chaotisch wäre. Auch die integrierte Partitionierung ist nur in der SQL Enterprise Edition verfügbar.

Wenn eine Partitionierung für Sie möglich ist, können Sie ein Partitionsschema wählen, das Ihre Daten in beispielsweise 8 verschiedene Sätze aufteilt. Jetzt könnten Sie Ihren ursprünglichen Single-Thread-Code verwenden, haben aber 8 Threads, die jeweils auf eine separate Partition abzielen. Jetzt gibt es keine (oder zumindest eine minimale Anzahl von) Deadlocks.

Ich hoffe das ergibt Sinn.