Dit is mijn kijk op het probleem:
-
Bij het gebruik van meerdere threads om gegevens in SQL Server of een database in te voegen/bij te werken/op te vragen, zijn impasses een feit. Je moet ervan uitgaan dat ze zich zullen voordoen en ze op de juiste manier behandelen.
-
Dat wil niet zeggen dat we niet moeten proberen het optreden van impasses te beperken. Het is echter gemakkelijk om de basisoorzaken van deadlocks te lezen en stappen te ondernemen om ze te voorkomen, maar SQL Server zal je altijd verrassen :-)
Enige reden voor impasses:
-
Te veel threads - probeer het aantal threads tot een minimum te beperken, maar we willen natuurlijk meer threads voor maximale prestaties.
-
Niet genoeg indexen. Als selecties en updates niet selectief genoeg zijn, zal SQL grotere bereikvergrendelingen uitschakelen dan in orde is. Probeer de juiste indexen op te geven.
-
Te veel indexen. Het bijwerken van indexen veroorzaakt impasses, dus probeer indexen tot het vereiste minimum te beperken.
-
Isolatieniveau voor transacties te hoog. Het standaard isolatieniveau bij gebruik van .NET is 'Serializable', terwijl het standaardniveau voor SQL Server 'Read Committed' is. Het verlagen van het isolatieniveau kan veel helpen (indien van toepassing natuurlijk).
Dit is hoe ik uw probleem zou kunnen aanpakken:
-
Ik zou mijn eigen threading-oplossing niet gebruiken, ik zou de TaskParallel-bibliotheek gebruiken. Mijn hoofdmethode zou er ongeveer zo uitzien:
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)); }
-
Voer de methode BerekenDetails uit met nieuwe pogingen voor mislukte impasses
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)); } }
-
De kernmethode van rekenenDetails
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(); } } }
-
En natuurlijk mijn implementatie van een helper voor het opnieuw proberen van een 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; } }
-
Een andere mogelijkheid is om een partitioneringsstrategie te gebruiken
Als uw tabellen natuurlijk kunnen worden gepartitioneerd in verschillende afzonderlijke gegevenssets, kunt u gepartitioneerde SQL Server-tabellen en -indexen gebruiken, of u kunt uw bestaande tabellen handmatig in verschillende sets tabellen splitsen. Ik zou aanraden om de partitionering van SQL Server te gebruiken, omdat de tweede optie rommelig zou zijn. Ook ingebouwde partitionering is alleen beschikbaar op SQL Enterprise Edition.
Als partitionering voor u mogelijk is, kunt u een partitieschema kiezen dat uw gegevens in, laten we zeggen 8 verschillende sets, opsplitst. Nu zou je je originele single-threaded code kunnen gebruiken, maar 8 threads hebben die elk gericht zijn op een aparte partitie. Nu zullen er geen (of op zijn minst een minimum aantal) impasses zijn.
Ik hoop dat dat logisch is.