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.