Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Applicazione C# multithreading con chiamate al database di SQL Server

Ecco la mia opinione sul problema:

  • Quando si utilizzano più thread per inserire/aggiornare/interrogare dati in SQL Server o in qualsiasi database, i deadlock sono un dato di fatto. Devi presumere che si verificheranno e gestirli in modo appropriato.

  • Non è così che non dovremmo tentare di limitare il verificarsi di deadlock. Tuttavia, è facile leggere le cause di base dei deadlock e adottare misure per prevenirli, ma SQL Server ti sorprenderà sempre :-)

Alcuni motivi per i deadlock:

  • Troppi thread:cerca di limitare il numero di thread al minimo, ma ovviamente vogliamo più thread per ottenere le massime prestazioni.

  • Indici insufficienti. Se le selezioni e gli aggiornamenti non sono sufficientemente selettivi, SQL eliminerà i blocchi di intervallo più grandi di quanto non sia salutare. Prova a specificare gli indici appropriati.

  • Troppi indici. L'aggiornamento degli indici provoca deadlock, quindi prova a ridurre gli indici al minimo richiesto.

  • Livello di isolamento della transazione troppo alto. Il livello di isolamento predefinito quando si usa .NET è "Serializzabile", mentre il livello predefinito che utilizza SQL Server è "Read Committed". Ridurre il livello di isolamento può aiutare molto (se appropriato ovviamente).

Ecco come potrei affrontare il tuo problema:

  • Non rollerei la mia soluzione di threading, userei la libreria TaskParallel. Il mio metodo principale sarebbe simile a questo:

    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));
    }
    
  • Esegui il metodo CalculateDetails con tentativi per deadlock non riusciti

    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));
        }
    }
    
  • Il metodo principale di 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();
            }
        }
    }
    
  • E, naturalmente, la mia implementazione di un helper per i tentativi di deadlock

    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;
        }
    }
    
  • Un'altra possibilità è utilizzare una strategia di partizionamento

Se le tabelle possono essere naturalmente partizionate in più set di dati distinti, è possibile utilizzare tabelle e indici partizionati di SQL Server oppure dividere manualmente le tabelle esistenti in più set di tabelle. Consiglierei di utilizzare il partizionamento di SQL Server, poiché la seconda opzione sarebbe disordinata. Anche il partizionamento integrato è disponibile solo in SQL Enterprise Edition.

Se il partizionamento è possibile per te, puoi scegliere uno schema di partizione che abbia suddiviso i tuoi dati in, diciamo, 8 insiemi distinti. Ora puoi usare il tuo codice a thread singolo originale, ma avere 8 thread ciascuno destinato a una partizione separata. Ora non ci saranno (o almeno un numero minimo di) deadlock.

Spero che abbia un senso.