Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Aplicativo C# multi-threading com chamadas de banco de dados SQL Server


Aqui está minha opinião sobre o problema:

  • Ao usar vários threads para inserir/atualizar/consultar dados no SQL Server ou em qualquer banco de dados, os deadlocks são um fato da vida. Você tem que assumir que eles ocorrerão e tratá-los adequadamente.

  • Isso não quer dizer que não devemos tentar limitar a ocorrência de deadlocks. No entanto, é fácil ler as causas básicas dos deadlocks e tomar medidas para evitá-los, mas o SQL Server sempre o surpreenderá :-)

Alguns motivos para impasses:

  • Muitos threads - tente limitar o número de threads ao mínimo, mas é claro que queremos mais threads para desempenho máximo.

  • Índices insuficientes. Se seleções e atualizações não forem seletivas o suficiente, o SQL removerá bloqueios de intervalo maiores do que o íntegro. Tente especificar índices apropriados.

  • Muitos índices. A atualização de índices causa deadlocks, portanto, tente reduzir os índices ao mínimo necessário.

  • Nível de isolamento da transação muito alto. O nível de isolamento padrão ao usar .NET é 'Serializável', enquanto o padrão usando o SQL Server é 'Leitura confirmada'. Reduzir o nível de isolamento pode ajudar muito (se for o caso, claro).

É assim que posso resolver seu problema:

  • Eu não lançaria minha própria solução de threading, usaria a biblioteca TaskParallel. Meu método principal ficaria assim:
    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));
    }
    

  • Execute o método CalculateDetails com novas tentativas para falhas de deadlock
    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));
        }
    }
    

  • O 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();
            }
        }
    }
    

  • E, claro, minha implementação de um auxiliar de repetição de 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;
        }
    }
    

  • Outra possibilidade é usar uma estratégia de particionamento

Se suas tabelas puderem ser particionadas naturalmente em vários conjuntos distintos de dados, você poderá usar tabelas e índices particionados do SQL Server ou dividir manualmente suas tabelas existentes em vários conjuntos de tabelas. Eu recomendaria usar o particionamento do SQL Server, pois a segunda opção seria confusa. O particionamento integrado também está disponível apenas no SQL Enterprise Edition.

Se o particionamento for possível para você, você pode escolher um esquema de partição que quebrou seus dados em, digamos, 8 conjuntos distintos. Agora você pode usar seu código de encadeamento único original, mas ter 8 encadeamentos, cada um direcionado a uma partição separada. Agora não haverá nenhum (ou pelo menos um número mínimo de) deadlocks.

Espero que faça sentido.