Här är min syn på problemet:
-
När du använder flera trådar för att infoga/uppdatera/fråga data i SQL Server, eller vilken databas som helst, är dödlägen ett faktum. Du måste anta att de kommer att inträffa och hantera dem på rätt sätt.
-
Det är inte så att vi inte ska försöka begränsa förekomsten av dödlägen. Det är dock lätt att läsa om de grundläggande orsakerna till dödlägen och vidta åtgärder för att förhindra dem, men SQL Server kommer alltid att överraska dig :-)
Någon anledning till dödläge:
-
För många trådar - försök att begränsa antalet trådar till ett minimum, men vi vill naturligtvis ha fler trådar för maximal prestanda.
-
Inte tillräckligt med index. Om val och uppdateringar inte är tillräckligt selektiva kommer SQL att ta ut större intervalllås än vad som är hälsosamt. Försök att ange lämpliga index.
-
För många index. Uppdatering av index orsakar dödlägen, så försök att minska indexen till det minimum som krävs.
-
Transaktionsisolationsnivån är för hög. Standardisoleringsnivån när du använder .NET är 'Serialiserbar', medan standarden som använder SQL Server är 'Read Committed'. Att minska isoleringsnivån kan hjälpa mycket (om det är lämpligt förstås).
Så här kan jag lösa ditt problem:
-
Jag skulle inte rulla min egen trådlösning, jag skulle använda TaskParallel-biblioteket. Min huvudsakliga metod skulle se ut ungefär så här:
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)); }
-
Kör metoden CalculateDetails med återförsök för dödlägesfel
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)); } }
-
Kärnmetoden 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(); } } }
-
Och naturligtvis min implementering av en dödlägesförsökshjälp
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 ytterligare möjlighet är att använda en partitioneringsstrategi
Om dina tabeller naturligt kan partitioneras i flera distinkta uppsättningar data, kan du antingen använda SQL Server-partitionerade tabeller och index, eller så kan du manuellt dela upp dina befintliga tabeller i flera uppsättningar tabeller. Jag skulle rekommendera att använda SQL Servers partitionering, eftersom det andra alternativet skulle vara rörigt. Även inbyggd partitionering är endast tillgänglig på SQL Enterprise Edition.
Om partitionering är möjligt för dig, kan du välja ett partitionsschema som bryter dina data i låt oss säga 8 distinkta uppsättningar. Nu kan du använda din ursprungliga entrådade kod, men ha 8 trådar var och en riktad mot en separat partition. Nu kommer det inte att finnas några (eller åtminstone ett minsta antal) dödlägen.
Jag hoppas att det är vettigt.