Her er mit bud på problemet:
-
Når du bruger flere tråde til at indsætte/opdatere/forespørge data i SQL Server eller en hvilken som helst database, så er deadlocks et faktum. Du er nødt til at antage, at de vil forekomme og håndtere dem korrekt.
-
Det er ikke sådan, at vi ikke skal forsøge at begrænse forekomsten af dødvande. Det er dog nemt at læse op om de grundlæggende årsager til dødvande og tage skridt til at forhindre dem, men SQL Server vil altid overraske dig :-)
Nogle årsager til dødvande:
-
For mange tråde - prøv at begrænse antallet af tråde til et minimum, men vi vil selvfølgelig gerne have flere tråde for maksimal ydeevne.
-
Ikke nok indekser. Hvis valg og opdateringer ikke er selektive nok, vil SQL fjerne større rækkeviddelåse end sundt. Prøv at angive passende indekser.
-
For mange indekser. Opdatering af indekser forårsager dødvande, så prøv at reducere indekser til det krævede minimum.
-
Transaktionens isolationsniveau er for højt. Standardisolationsniveauet ved brug af .NET er 'Serialiserbar', mens standarden ved brug af SQL Server er 'Read Committed'. At reducere isolationsniveauet kan hjælpe meget (hvis det er relevant selvfølgelig).
Sådan kan jeg løse dit problem:
-
Jeg ville ikke rulle min egen trådløsning, jeg ville bruge TaskParallel-biblioteket. Min primære metode ville se sådan ud:
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)); }
-
Udfør CalculateDetails-metoden med genforsøg for deadlock-fejl
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)); } }
-
Kernen CalculateDetails-metoden
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(); } } }
-
Og selvfølgelig min implementering af en deadlock genforsøgshjælper
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; } }
-
En yderligere mulighed er at bruge en partitioneringsstrategi
Hvis dine tabeller naturligt kan opdeles i flere forskellige sæt data, kan du enten bruge SQL Server-partitionerede tabeller og indekser, eller du kan manuelt opdele dine eksisterende tabeller i flere sæt tabeller. Jeg vil anbefale at bruge SQL Servers partitionering, da den anden mulighed ville være rodet. Også indbygget partitionering er kun tilgængelig på SQL Enterprise Edition.
Hvis partitionering er muligt for dig, kan du vælge et partitionsskema, der bryder dine data i lad os sige 8 forskellige sæt. Nu kan du bruge din originale enkelttrådede kode, men have 8 tråde, der hver er målrettet mod en separat partition. Nu vil der ikke være nogen (eller i det mindste et minimum antal) dødvande.
Jeg håber, det giver mening.