Condividi tramite


Resilienza delle connessioni

La resilienza della connessione ritenta automaticamente i comandi del database non riusciti. La funzionalità può essere usata con qualsiasi database fornendo una "strategia di esecuzione", che incapsula la logica necessaria per rilevare gli errori e ripetere i comandi. I provider EF Core possono fornire strategie di esecuzione personalizzate in base alle specifiche condizioni di errore del database e ai criteri di ripetizione ottimali dei tentativi.

Ad esempio, il provider SQL Server include una strategia di esecuzione specificamente personalizzata per SQL Server (incluso SQL Azure). È consapevole dei tipi di eccezione che possono essere ripetuti e ha impostazioni predefinite ragionevoli per i tentativi massimi, il ritardo tra i tentativi e così via.

Quando si configurano le opzioni per il contesto, viene specificata una strategia di esecuzione. Questo si trova tipicamente nel metodo OnConfiguring del contesto derivato.

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

o in Startup.cs per un'applicazione ASP.NET Core:

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

Nota

L'abilitazione del ritentativo in caso di errore fa sì che Entity Framework memorizzi internamente il set di risultati, il che può aumentare significativamente i requisiti di memoria per le query che restituiscono set di risultati di grandi dimensioni. Per ulteriori dettagli, vedere buffering e streaming.

Strategia di esecuzione personalizzata

Esiste un meccanismo per registrare una strategia di esecuzione personalizzata se si vuole modificare uno dei valori predefiniti.

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

Strategie di esecuzione e transazioni

Una strategia di esecuzione che ritenta automaticamente gli errori deve essere in grado di riprodurre ogni operazione in un blocco di tentativi che ha esito negativo. Quando i tentativi sono abilitati, ogni operazione eseguita tramite EF Core diventa una propria operazione di ripetizione. Cioè, ogni query e ogni chiamata a SaveChangesAsync() verrà ritentata nel suo insieme se si verifica un errore temporaneo.

Tuttavia, se il codice avvia una transazione usando BeginTransactionAsync(), si sta definendo il proprio gruppo di operazioni che devono essere considerate come un'unità e tutto ciò che si trova all'interno della transazione deve essere ri-eseguito in caso di errore. Se si tenta di eseguire questa operazione quando si usa una strategia di esecuzione, si riceverà un'eccezione simile alla seguente:

InvalidOperationException: la strategia di esecuzione configurata 'SqlServerRetryingExecutionStrategy' non supporta le transazioni avviate dall'utente. Usare la strategia di esecuzione restituita da 'DbContext.Database.CreateExecutionStrategy()' per eseguire tutte le operazioni nella transazione come unità ritentabile.

La soluzione consiste nell'richiamare manualmente la strategia di esecuzione con un delegato che rappresenta tutto ciò che deve essere eseguito. Se si verifica un errore temporaneo, la strategia di esecuzione richiamerà nuovamente il delegato.


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

Questo approccio può essere usato anche con le transazioni ambientali.


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

Errore di commit della transazione e problema di idempotenza

In generale, quando si verifica un errore di connessione, viene eseguito il rollback della transazione corrente. Tuttavia, se la connessione viene eliminata mentre viene eseguito il commit della transazione, lo stato risultante della transazione è sconosciuto.

Per impostazione predefinita, la strategia di esecuzione ritenta l'operazione come se fosse stato eseguito il rollback della transazione, ma se non si tratta di un caso questo genererà un'eccezione se il nuovo stato del database è incompatibile o potrebbe causare danneggiamento dei dati se l'operazione non si basa su uno stato specifico, ad esempio quando si inserisce una nuova riga con valori di chiave generati automaticamente.

Esistono diversi modi per gestire questo problema.

Opzione 1 - Non fare (quasi) nulla

La probabilità di un errore di connessione durante il commit della transazione è bassa, pertanto può essere accettabile che l'applicazione non riesca se questa condizione si verifica effettivamente.

Tuttavia, è necessario evitare di usare chiavi generate dal sistema di archiviazione per assicurarsi che venga generata un'eccezione invece di aggiungere una riga duplicata. Prendere in considerazione l'uso di un valore GUID generato dal client o di un generatore di valori del lato cliente.

Opzione 2 - Ricompilare lo stato dell'applicazione

  1. Rimuovere il DbContextcorrente.
  2. Creare un nuovo DbContext e ripristinare lo stato dell'applicazione dal database.
  3. Informare l'utente che l'ultima operazione potrebbe non essere stata completata correttamente.

Opzione 3 - Aggiungere la verifica dello stato

Per la maggior parte delle operazioni che modificano lo stato del database, è possibile aggiungere codice che controlla se ha avuto esito positivo. Ef fornisce un metodo di estensione per semplificare questa operazione, IExecutionStrategy.ExecuteInTransaction.

Questo metodo inizia e esegue il commit di una transazione e accetta anche una funzione nel parametro verifySucceeded richiamato quando si verifica un errore temporaneo durante il commit della transazione.


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

Nota

In questo SaveChanges viene richiamato con acceptAllChangesOnSuccess impostato su false per evitare di modificare lo stato dell'entità Blog a Unchanged nel caso in cui SaveChanges abbia esito positivo. In questo modo è possibile ritentare la stessa operazione se il commit ha esito negativo e viene eseguito il rollback della transazione.

Opzione 4 - Tenere traccia manuale della transazione

Se è necessario usare chiavi generate dall'archivio o è necessario un modo generico per gestire gli errori di commit che non dipendono dall'operazione eseguita a ogni transazione potrebbe essere assegnato un ID controllato quando il commit ha esito negativo.

  1. Aggiungere una tabella al database utilizzato per tenere traccia dello stato delle transazioni.
  2. Inserire una riga nella tabella all'inizio di ogni transazione.
  3. Se la connessione non riesce durante il commit, verificare la presenza della riga corrispondente nel database.
  4. Se il commit ha esito positivo, eliminare la riga corrispondente per evitare la crescita della tabella.

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

Nota

Assicurarsi che il contesto usato per la verifica disponga di una strategia di esecuzione definita, poiché è probabile che la connessione possa fallire nuovamente durante la verifica se è fallita durante il commit della transazione.

Risorse aggiuntive