sql >> Base de Datos >  >> RDS >> Sqlserver

Aplicación C# multiproceso con llamadas a la base de datos de SQL Server

Esta es mi opinión sobre el problema:

  • Cuando se utilizan varios subprocesos para insertar/actualizar/consultar datos en SQL Server o en cualquier base de datos, los interbloqueos son una realidad. Tienes que asumir que ocurrirán y manejarlos apropiadamente.

  • Eso no significa que no debamos intentar limitar la aparición de interbloqueos. Sin embargo, es fácil leer sobre las causas básicas de los interbloqueos y tomar medidas para evitarlos, pero SQL Server siempre lo sorprenderá :-)

Algunas razones para los interbloqueos:

  • Demasiados subprocesos:intente limitar la cantidad de subprocesos al mínimo, pero, por supuesto, queremos más subprocesos para obtener el máximo rendimiento.

  • No hay suficientes índices. Si las selecciones y las actualizaciones no son lo suficientemente selectivas, SQL eliminará bloqueos de rango más grandes de lo que es saludable. Intente especificar los índices apropiados.

  • Demasiados índices. La actualización de índices provoca interbloqueos, así que intente reducir los índices al mínimo necesario.

  • Nivel de aislamiento de transacción demasiado alto. El nivel de aislamiento predeterminado cuando se usa .NET es 'Serializable', mientras que el predeterminado con SQL Server es 'Read Committed'. Reducir el nivel de aislamiento puede ayudar mucho (si es apropiado, por supuesto).

Así es como podría abordar su problema:

  • No lanzaría mi propia solución de subprocesos, usaría la biblioteca TaskParallel. Mi método principal se vería así:

    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));
    }
    
  • Ejecute el método CalculateDetails con reintentos para fallas de interbloqueo

    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));
        }
    }
    
  • El método principal 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();
            }
        }
    }
    
  • Y, por supuesto, mi implementación de un asistente de reintento de punto muerto

    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;
        }
    }
    
  • Otra posibilidad es utilizar una estrategia de partición

Si sus tablas se pueden particionar naturalmente en varios conjuntos distintos de datos, entonces puede usar tablas e índices particionados de SQL Server, o puede dividir manualmente sus tablas existentes en varios conjuntos de tablas. Recomendaría usar el particionamiento de SQL Server, ya que la segunda opción sería complicada. Además, la partición integrada solo está disponible en SQL Enterprise Edition.

Si la partición es posible para usted, puede elegir un esquema de partición que divida sus datos en, digamos, 8 conjuntos distintos. Ahora podría usar su código de subproceso único original, pero tener 8 subprocesos, cada uno dirigido a una partición separada. Ahora no habrá ningún punto muerto (o al menos un número mínimo de ellos).

Espero que tenga sentido.