Verbindungsresilienz
Die Verbindungsresilienz versucht fehlgeschlagene Datenbankbefehle automatisch erneut. Das Feature kann mit jeder Datenbank verwendet werden, indem eine "Ausführungsstrategie" bereitgestellt wird, die die Logik kapselt, die zum Erkennen von Fehlern und Wiederholungsbefehlen erforderlich ist. EF Core-Anbieter können Ausführungsstrategien bereitstellen, die auf ihre spezifischen Datenbankfehlerbedingungen und optimale Wiederholungsrichtlinien zugeschnitten sind.
Beispielsweise enthält der SQL Server-Anbieter eine Ausführungsstrategie, die speziell auf SQL Server (einschließlich SQL Azure) zugeschnitten ist. Dabei sind die Ausnahmetypen bekannt, die wiederholt werden können, und es liegen sinnvolle Standardwerte für maximale Wiederholungsversuche, Verzögerung zwischen Wiederholungen usw. vor.
Eine Ausführungsstrategie wird beim Konfigurieren der Optionen für den Kontext angegeben. Dies geschieht sich in der Regel in der OnConfiguring
-Methode des abgeleiteten Kontexts:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True;ConnectRetryCount=0",
options => options.EnableRetryOnFailure());
}
Bei einer bestehenden ASP.NET Core-Anwendung geschieht dies stattdessen in Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<PicnicContext>(
options => options.UseSqlServer(
"<connection string>",
providerOptions => providerOptions.EnableRetryOnFailure()));
}
Anmerkung
Das Aktivieren von Wiederholungen bei Fehlern führt dazu, dass EF das Resultset intern puffert. Dies kann zu erheblich erhöhten Speicheranforderungen für Abfragen, die große Resultsets zurückgeben, führen. Weitere Informationen finden Sie unter Puffern und Streaming.
Benutzerdefinierte Ausführungsstrategie
Es gibt einen Mechanismus zum Registrieren einer eigenen benutzerdefinierten Ausführungsstrategie, wenn Sie einen der Standardwerte ändern möchten.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMyProvider(
"<connection string>",
options => options.ExecutionStrategy(...));
}
Ausführungsstrategien und Transaktionen
Eine Ausführungsstrategie, bei der bei Fehlern automatische Wiederholungen stattfinden, muss alle Vorgänge in einem fehlerhaften Wiederholungsblock wiedergeben können. Wenn Wiederholungsversuche aktiviert sind, wird jeder Vorgang, den Sie über EF Core ausführen, zu einem eigenen wiederholungsfähigen Vorgang. Jede Abfrage und jeder Aufruf von SaveChangesAsync()
wird somit als eine Einheit wiederholt, wenn ein vorübergehender Fehler auftritt.
Wenn Ihr Code jedoch eine Transaktion mit BeginTransactionAsync()
initiiert, definieren Sie Ihre eigene Gruppe von Vorgängen, die als Einheit behandelt werden müssen, und alles innerhalb der Transaktion muss wiederhergestellt werden, falls ein Fehler auftritt. Sie erhalten eine Ausnahme wie die folgende, wenn Sie dies bei Verwendung einer Ausführungsstrategie versuchen:
InvalidOperationException: Die konfigurierte Ausführungsstrategie „SqlServerRetryingExecutionStrategy“ unterstützt keine von Benutzer*innen instanziierten Transaktionen. Verwenden Sie die Ausführungsstrategie, die von 'DbContext.Database.CreateExecutionStrategy()' zurückgegeben wird, um alle Vorgänge in der Transaktion als eine wiederholbare Einheit auszuführen.
Die Lösung besteht darin, die Ausführungsstrategie mit einem Delegaten manuell aufzurufen, der alle Komponenten darstellt, die ausgeführt werden müssen. Die Ausführungsstrategie ruft den Delegaten erneut auf, wenn ein vorübergehender Fehler auftritt.
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();
});
Dieser Ansatz kann auch bei Umgebungstransaktionen verwendet werden.
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();
});
Transaktionscommit-Fehler und das Idempotenz-Problem
Im Allgemeinen wird die aktuelle Transaktion zurückgesetzt, wenn ein Verbindungsfehler auftritt. Falls jedoch die Verbindung abgebrochen wird, während die Transaktion ausgeführt wird, ist der resultierende Zustand der Transaktion unbekannt.
Standardmäßig wiederholt die Ausführungsstrategie den Vorgang, als wäre die Transaktion zurückgerollt worden. Ist dies jedoch nicht der Fall, wird eine Ausnahme ausgelöst, wenn der neue Datenbankstatus inkompatibel ist oder zu Datenbeschädigung führen kann, wenn der Vorgang nicht von einem bestimmten Zustand abhängt, zum Beispiel beim Einfügen einer neuen Zeile mit automatisch generierten Schlüsselwerten.
Es gibt mehrere Möglichkeiten, dies zu bewältigen.
Option 1 – Nichts tun (fast)
Die Wahrscheinlichkeit eines Verbindungsfehlers während des Transaktionscommits ist niedrig, sodass es für Ihre Anwendung akzeptabel sein kann, fehlzuschlagen, wenn diese Bedingung tatsächlich auftritt.
Sie müssen jedoch die Verwendung von im Speicher generierten Schlüsseln vermeiden, um sicherzustellen, dass eine Ausnahme ausgelöst wird, anstatt eine doppelte Zeile hinzuzufügen. Erwägen Sie die Verwendung eines clientseitig generierten GUID-Werts oder eines clientseitigen Wertgenerators.
Option 2 – Neuerstellen des Anwendungszustands
- Verwerfen Sie den aktuellen
DbContext
. - Erstellen Sie einen neuen
DbContext
, und stellen Sie den Status Ihrer Anwendung aus der Datenbank wieder her. - Informieren Sie den Benutzer, dass der letzte Vorgang möglicherweise nicht erfolgreich abgeschlossen wurde.
Option 3 – Statusüberprüfung hinzufügen
Für die meisten Vorgänge, die den Datenbankstatus ändern, ist es möglich, Code hinzuzufügen, der überprüft, ob er erfolgreich war. EF bietet eine Erweiterungsmethode, um dies zu vereinfachen: IExecutionStrategy.ExecuteInTransaction
.
Diese Methode führt zunächst einen Commit für eine Transaktion durch und akzeptiert außerdem eine Funktion im verifySucceeded
-Parameter, die aufgerufen wird, wenn während des Transaktionscommits ein vorübergehender Fehler auftritt.
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();
Anmerkung
Hier wird SaveChanges
aufgerufen, wobei acceptAllChangesOnSuccess
auf false
festgelegt ist, um zu vermeiden, dass der Status der Blog
Entität in Unchanged
geändert wird, wenn SaveChanges
erfolgreich ist. Dadurch kann derselbe Vorgang erneut ausgeführt werden, wenn der Commit fehlschlägt und die Transaktion zurückgesetzt wird.
Option 4 – Manuelles Nachverfolgen der Transaktion
Wenn Sie speichergenerierte Schlüssel verwenden müssen oder eine allgemeine Methode zum Umgang mit Commit-Fehlern benötigen, die nicht von dem ausgeführten Vorgang abhängt, könnte jeder Transaktion eine ID zugewiesen werden, die überprüft wird, wenn der Commit fehlschlägt.
- Fügen Sie der Datenbank eine Tabelle hinzu, die zum Nachverfolgen des Status der Transaktionen verwendet wird.
- Fügen Sie am Anfang jeder Transaktion eine Zeile in die Tabelle ein.
- Wenn die Verbindung während des Commits fehlschlägt, überprüfen Sie, ob die entsprechende Zeile in der Datenbank vorhanden ist.
- Wenn der Commit erfolgreich ist, löschen Sie die entsprechende Zeile, um das Wachstum der Tabelle zu vermeiden.
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();
Anmerkung
Stellen Sie sicher, dass der Kontext, der für die Überprüfung verwendet wird, über eine definierte Ausführungsstrategie verfügt. Dies ist wichtig, da die Verbindung während der Überprüfung möglicherweise erneut fehlschlägt, wenn sie bereits beim Transaktions-Commit fehlgeschlagen ist.