Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Application C # multi-threading avec appels de base de données SQL Server

Voici mon point de vue sur le problème :

  • Lorsque vous utilisez plusieurs threads pour insérer/mettre à jour/interroger des données dans SQL Server ou dans n'importe quelle base de données, les blocages font partie de la vie. Vous devez supposer qu'ils se produiront et les gérer de manière appropriée.

  • Cela ne veut pas dire qu'il ne faut pas tenter de limiter l'apparition de blocages. Cependant, il est facile de lire les causes fondamentales des blocages et de prendre des mesures pour les éviter, mais SQL Server vous surprendra toujours :-)

Certaines raisons des blocages :

  • Trop de threads - essayez de limiter le nombre de threads au minimum, mais nous voulons bien sûr plus de threads pour des performances maximales.

  • Pas assez d'index. Si les sélections et les mises à jour ne sont pas suffisamment sélectives, SQL supprimera des verrous de plage plus importants que ce qui est sain. Essayez de spécifier les index appropriés.

  • Trop d'index. La mise à jour des index provoque des blocages, essayez donc de réduire les index au minimum requis.

  • Niveau d'isolement de la transaction trop élevé. Le niveau d'isolement par défaut lors de l'utilisation de .NET est «Serializable», tandis que le niveau par défaut avec SQL Server est «Read Committed». Réduire le niveau d'isolement peut aider beaucoup (le cas échéant bien sûr).

Voici comment je pourrais résoudre votre problème :

  • Je ne lancerais pas ma propre solution de threading, j'utiliserais la bibliothèque TaskParallel. Ma méthode principale ressemblerait à ceci :

    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));
    }
    
  • Exécutez la méthode CalculateDetails avec de nouvelles tentatives pour les échecs de blocage

    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));
        }
    }
    
  • La méthode de base 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();
            }
        }
    }
    
  • Et bien sûr, mon implémentation d'un assistant de relance de blocage

    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;
        }
    }
    
  • Une autre possibilité consiste à utiliser une stratégie de partitionnement

Si vos tables peuvent naturellement être partitionnées en plusieurs ensembles de données distincts, vous pouvez soit utiliser des tables et des index partitionnés SQL Server, soit diviser manuellement vos tables existantes en plusieurs ensembles de tables. Je recommanderais d'utiliser le partitionnement de SQL Server, car la deuxième option serait désordonnée. De plus, le partitionnement intégré n'est disponible que sur SQL Enterprise Edition.

Si le partitionnement est possible pour vous, vous pouvez choisir un schéma de partition qui divise vos données en disons 8 ensembles distincts. Vous pouvez maintenant utiliser votre code à un seul thread d'origine, mais avoir 8 threads ciblant chacun une partition distincte. Désormais, il n'y aura plus de blocages (ou au moins un nombre minimum de blocages).

J'espère que cela à du sens.