Dela via


Anslutningsresiliens

Anslutningsresiliens gör automatiskt nya försök att köra misslyckade databaskommandon. Funktionen kan användas med valfri databas genom att tillhandahålla en "körningsstrategi", som kapslar in den logik som krävs för att identifiera fel och försöka kommandon igen. EF Core-leverantörer kan tillhandahålla utförandestrategier som är skräddarsydda för deras specifika villkor för databasfel och optimala riktlinjer för återförsök.

Sql Server-providern innehåller till exempel en körningsstrategi som är specifikt anpassad till SQL Server (inklusive SQL Azure). Den är medveten om de undantagstyper som kan göras om och har lämpliga standardvärden för maximala återförsök, fördröjning mellan återförsök osv.

En körningsstrategi anges när du konfigurerar alternativen för din kontext. Detta är vanligtvis i OnConfiguring-metoden för ditt avledda sammanhang:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True;ConnectRetryCount=0",
            options => options.EnableRetryOnFailure());
}

eller i Startup.cs för ett ASP.NET Core-program:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<PicnicContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));
}

Not

Om du aktiverar återförsök vid fel kan EF internt buffra resultatuppsättningen, vilket avsevärt kan öka minneskraven för frågor som returnerar stora resultatuppsättningar. Mer information finns i buffring och strömning.

Anpassad körningsstrategi

Det finns en mekanism för att registrera en egen anpassad körningsstrategi om du vill ändra någon av standardvärdena.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseMyProvider(
            "<connection string>",
            options => options.ExecutionStrategy(...));
}

Utförandesstrategier och transaktioner

En körningsstrategi som automatiskt försöker igen vid fel måste kunna upprepa varje åtgärd i ett block för återförsök som misslyckas. När återförsök är aktiverade blir varje åtgärd som du utför via EF Core en egen åtgärd som kan göras om. Det vill säga att varje fråga och varje anrop till SaveChangesAsync() görs om som en enhet om ett tillfälligt fel inträffar.

Men om koden initierar en transaktion med BeginTransactionAsync() definierar du en egen grupp med åtgärder som måste behandlas som en enhet, och allt i transaktionen skulle behöva spelas upp om ett fel skulle inträffa. Du får ett undantag som liknar följande om du försöker göra detta när du använder en körningsstrategi:

InvalidOperationException: Den konfigurerade exekveringsstrategin "SqlServerRetryingExecutionStrategy" stöder inte användarinitierade transaktioner. Använd körningsstrategin som returneras av "DbContext.Database.CreateExecutionStrategy()" för att köra alla åtgärder i transaktionen som en åtgärd som kan upprepas.

Lösningen är att manuellt anropa strategin för exekvering med en delegering som representerar allt som behöver köras. Om ett tillfälligt fel inträffar kommer körningsstrategin att anropa delegeringen igen.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(
    async () =>
    {
        using var context = new BloggingContext();
        await using var transaction = await context.Database.BeginTransactionAsync();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context.SaveChangesAsync();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    });

Den här metoden kan också användas med omgivande transaktioner.


using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });

var strategy = context1.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(
    async () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

        context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context2.SaveChangesAsync();

        await context1.SaveChangesAsync();

        transaction.Complete();
    });

Transaktionens misslyckande vid genomförande och frågan om idempotens

I allmänhet återställs den aktuella transaktionen när det uppstår ett anslutningsfel. Men om anslutningen tappas medan transaktionen bekräftas är det resulterande tillståndet för transaktionen okänt.

Som standard försöker körningsstrategin åtgärden igen som om transaktionen återställdes, men om så inte är fallet resulterar detta i ett undantag om det nya databastillståndet är inkompatibelt eller kan leda till skadade data om åtgärden inte förlitar sig på ett visst tillstånd, till exempel när du infogar en ny rad med automatiskt genererade nyckelvärden.

Det finns flera sätt att hantera detta.

Alternativ 1 – Gör (nästan) ingenting

Sannolikheten för ett anslutningsfel under transaktionen är låg, så det kan vara acceptabelt att ditt program kan misslyckas om detta villkor faktiskt inträffar.

Du måste dock undvika att använda butiksgenererade nycklar för att säkerställa att ett undantag utlöses i stället för att lägga till en dubblettrad. Överväg att använda ett klientgenererat GUID-värde eller en värdegenerator på klientsidan.

Alternativ 2 – Återskapa programtillstånd

  1. Ta bort den aktuella DbContext.
  2. Skapa en ny DbContext och återställ programmets tillstånd från databasen.
  3. Informera användaren om att den senaste åtgärden kanske inte har slutförts.

Alternativ 3 – Lägg till tillståndsverifiering

För de flesta åtgärder som ändrar databastillståndet är det möjligt att lägga till kod som kontrollerar om den lyckades. EF tillhandahåller en tilläggsmetod för att göra detta enklare – IExecutionStrategy.ExecuteInTransaction.

Den här metoden börjar och genomför en transaktion och accepterar även en funktion i parametern verifySucceeded som anropas när ett tillfälligt fel inträffar under transaktionsincheckningen.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
db.Blogs.Add(blogToAdd);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Blogs.AsNoTracking().AnyAsync(b => b.BlogId == blogToAdd.BlogId, cancellationToken));

db.ChangeTracker.AcceptAllChanges();

Anteckning

Här anropas SaveChanges med acceptAllChangesOnSuccess inställt på false för att undvika att ändra tillståndet för den Blog entiteten till Unchanged om SaveChanges lyckas. På så sätt kan du försöka utföra samma åtgärd igen om incheckningen misslyckas och transaktionen återställs.

Alternativ 4 – Spåra transaktionen manuellt

Om du behöver använda butiksgenererade nycklar eller behöver ett allmänt sätt att hantera incheckningsfel som inte är beroende av den åtgärd som utförs kan varje transaktion tilldelas ett ID som kontrolleras när incheckningen misslyckas.

  1. Lägg till en tabell i databasen som används för att spåra transaktionernas status.
  2. Infoga en rad i tabellen i början av varje transaktion.
  3. Om anslutningen misslyckas under bekräftelsen, kontrollera om motsvarande rad finns i databasen.
  4. Om incheckningen lyckas tar du bort motsvarande rad för att undvika tabellens tillväxt.

using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });

var transaction = new TransactionRow { Id = Guid.NewGuid() };
db.Transactions.Add(transaction);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Transactions.AsNoTracking().AnyAsync(t => t.Id == transaction.Id, cancellationToken));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
await db.SaveChangesAsync();

Anteckning

Kontrollera att kontexten som används för verifieringen har en körningsstrategi definierad eftersom anslutningen sannolikt kommer att misslyckas igen under verifieringen om den misslyckades under transaktionsgenomförandet.

Ytterligare resurser