Udostępnij za pośrednictwem


Odporność połączenia

Odporność połączenia automatycznie ponawia próby nieudanych poleceń bazy danych. Funkcji można używać z dowolną bazą danych, podając "strategię wykonywania", która hermetyzuje logikę niezbędną do wykrywania błędów i ponawiania prób poleceń. Dostawcy platformy EF Core mogą dostarczać strategie wykonywania dostosowane do określonych warunków awarii bazy danych i optymalne zasady ponawiania prób.

Na przykład dostawca programu SQL Server zawiera strategię wykonywania, która jest specjalnie dopasowana do programu SQL Server (w tym sql Azure). Jest świadomy typów wyjątków, które można ponowić i ma rozsądne wartości domyślne dla maksymalnych ponownych prób, opóźnienia między ponownymi próbami itp.

Strategia wykonywania jest określana w trakcie konfigurowania opcji dla kontekstu. Zazwyczaj w kontekście pochodnym jest to metoda OnConfiguring:

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

lub w Startup.cs dla aplikacji ASP.NET Core:

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

Notatka

Włączenie ponawiania próby po awarii powoduje, że program EF wewnętrznie buforuje zestaw wyników, co może znacznie zwiększyć wymagania dotyczące pamięci dla zapytań zwracających duże zestawy wyników. Aby uzyskać więcej informacji, zobacz buforowanie i przesyłanie strumieniowe.

Niestandardowa strategia wykonywania

Istnieje mechanizm rejestrowania własnej niestandardowej strategii wykonywania, jeśli chcesz zmienić dowolną z wartości domyślnych.

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

Strategie egzekucji i transakcje

Strategia wykonywania, która automatycznie ponawia próby w przypadku niepowodzeń, musi być w stanie odtworzyć każdą operację w bloku ponawiania, który kończy się niepowodzeniem. Po włączeniu ponownych prób każda operacja wykonywana za pośrednictwem programu EF Core staje się własną operacją ponawiania prób. Oznacza to, że każde zapytanie i każde wywołanie SaveChangesAsync() będą ponownie wykonane razem, jeśli wystąpi błąd przejściowy.

Jeśli jednak twój kod inicjuje transakcję przy użyciu BeginTransactionAsync(), oznacza to, że tworzysz własną grupę operacji, które muszą być traktowane jako całość, a wszystko wewnątrz transakcji musi zostać ponownie odtworzone, jeśli wystąpi awaria. Jeśli spróbujesz to zrobić podczas korzystania ze strategii wykonywania, otrzymasz wyjątek podobny do następującego:

InvalidOperationException: skonfigurowana strategia wykonywania "SqlServerRetryingExecutionStrategy" nie obsługuje transakcji inicjowanych przez użytkownika. Użyj strategii wykonywania, którą zwraca polecenie "DbContext.Database.CreateExecutionStrategy()", aby wykonać wszystkie operacje w transakcji jako jednostkę, którą można ponawiać.

Rozwiązaniem jest ręczne wywołanie strategii wykonania przy użyciu delegata, który reprezentuje wszystko, co należy wykonać. Jeśli wystąpi błąd przejściowy, strategia wykonywania ponownie wywoła delegata.


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();
    });

To podejście można również stosować w przypadku transakcji środowiskowych.


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();
    });

Niepowodzenie zatwierdzania transakcji i problem z idempotencją

Ogólnie rzecz biorąc, gdy wystąpi błąd połączenia, bieżąca transakcja zostanie wycofana. Jeśli jednak połączenie zostanie przerwane, gdy transakcja jest zatwierdzana, wynikowy stan transakcji jest nieznany.

Domyślnie strategia wykonawcza próbuje ponowić operację, jakby transakcja została wycofana, ale jeśli tak nie jest, spowoduje to wyjątek, jeśli nowy stan bazy danych jest niekompatybilny, albo może prowadzić do uszkodzenia danych, jeśli operacja nie zależy od określonego stanu, na przykład podczas wstawiania nowego wiersza z wartościami klucza generowanego automatycznie.

Istnieje kilka sposobów radzenia sobie z tym.

Opcja 1 — Nie rób (prawie) nic

Prawdopodobieństwo niepowodzenia połączenia podczas zatwierdzania transakcji jest niskie, więc może być akceptowalne, aby aplikacja mogła po prostu zakończyć się niepowodzeniem, jeśli ten warunek rzeczywiście wystąpi.

Należy jednak unikanie używania kluczy generowanych przez bazę danych, aby upewnić się, że wyjątek jest wyrzucany zamiast dodawania zduplikowanego wiersza. Rozważ użycie wartości identyfikatora GUID wygenerowanego przez klienta lub generatora wartości po stronie klienta.

Opcja 2 — ponowne kompilowanie stanu aplikacji

  1. Odrzuć bieżący DbContext.
  2. Utwórz nowy DbContext i przywróć stan aplikacji z bazy danych.
  3. Poinformuj użytkownika, że ostatnia operacja mogła nie zostać ukończona pomyślnie.

Opcja 3 — Dodawanie weryfikacji stanu

W przypadku większości operacji, które zmieniają stan bazy danych, można dodać kod sprawdzający, czy zakończył się pomyślnie. Program EF udostępnia metodę rozszerzenia, aby ułatwić ten proces — IExecutionStrategy.ExecuteInTransaction.

Ta metoda rozpoczyna i zatwierdza transakcję, a także akceptuje funkcję w parametrze verifySucceeded wywoływanym w przypadku wystąpienia błędu przejściowego podczas zatwierdzania transakcji.


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();

Notatka

Tutaj SaveChanges jest wywoływane z acceptAllChangesOnSuccess ustawionym na false, aby uniknąć zmiany stanu obiektu Blog na Unchanged, jeśli SaveChanges się powiedzie. Dzięki temu można ponowić próbę wykonania tej samej operacji, jeśli zatwierdzenie zakończy się niepowodzeniem, a transakcja zostanie wycofana.

Opcja 4 — ręczne śledzenie transakcji

Jeśli musisz używać kluczy generowanych przez system lub potrzebujesz ogólnego sposobu obsługi niepowodzeń podczas zatwierdzania, który nie zależy od operacji wykonanej w każdej transakcji, każdej transakcji można przypisać identyfikator, który jest sprawdzany w przypadku niepowodzenia zatwierdzenia.

  1. Dodaj tabelę do bazy danych używanej do śledzenia stanu transakcji.
  2. Wstaw wiersz do tabeli na początku każdej transakcji.
  3. Jeśli połączenie nie powiedzie się podczas zatwierdzania, sprawdź obecność odpowiedniego wiersza w bazie danych.
  4. Jeśli zatwierdzenie zakończy się pomyślnie, usuń odpowiedni wiersz, aby uniknąć wzrostu tabeli.

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();

Notatka

Upewnij się, że kontekst używany do weryfikacji ma zdefiniowaną strategię wykonywania, ponieważ połączenie prawdopodobnie nie powiedzie się podczas weryfikacji, jeśli nie powiodło się podczas zatwierdzania transakcji.

Dodatkowe zasoby