Partilhar via


Resiliência da conexão

A resiliência da conexão repete automaticamente os comandos de banco de dados com falha. O recurso pode ser usado com qualquer banco de dados, fornecendo uma "estratégia de execução", que encapsula a lógica necessária para detetar falhas e repetir comandos. Os provedores do EF Core podem fornecer estratégias de execução adaptadas às suas condições específicas de falha de banco de dados e políticas ideais de repetição.

Como exemplo, o provedor do SQL Server inclui uma estratégia de execução especificamente adaptada ao SQL Server (incluindo o SQL Azure). Ele está ciente dos tipos de exceção que podem ser repetidos e tem padrões sensatos para tentativas máximas, atraso entre tentativas, etc.

Uma estratégia de execução é especificada ao configurar as opções para o seu contexto. Isso geralmente ocorre no método OnConfiguring do seu contexto derivado:

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

ou em Startup.cs para um aplicativo ASP.NET Core:

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

Observação

Habilitar a repetição em caso de falha faz com que o EF armazene internamente em buffer o conjunto de resultados, o que pode aumentar significativamente os requisitos de memória para consultas que retornam grandes conjuntos de resultados. Consulte buffering e streaming para obter mais detalhes.

Estratégia de execução personalizada

Há um mecanismo para registrar uma estratégia de execução personalizada própria se você deseja alterar qualquer um dos padrões.

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

Estratégias de execução e transações

Uma estratégia de execução que tenta novamente automaticamente em caso de falhas precisa ser capaz de reproduzir cada operação em um bloco de repetição que falha. Quando as novas tentativas são habilitadas, cada operação executada via EF Core se torna sua própria operação recuperável. Ou seja, cada consulta e cada chamada para SaveChangesAsync() será tentada novamente como uma unidade se ocorrer uma falha transitória.

No entanto, se o seu código inicia uma transação usando BeginTransactionAsync() você está definindo seu próprio grupo de operações que precisam ser tratadas como uma unidade, e tudo dentro da transação precisaria ser reproduzido caso ocorra uma falha. Você receberá uma exceção como a seguinte se tentar fazer isso ao usar uma estratégia de execução:

InvalidOperationException: A estratégia de execução configurada 'SqlServerRetryingExecutionStrategy' não suporta transações iniciadas pelo usuário. Use a estratégia de execução retornada por 'DbContext.Database.CreateExecutionStrategy()' para executar todas as operações na transação como uma unidade recuperável.

A solução é invocar manualmente a estratégia de execução com um delegado representando tudo o que precisa ser executado. Se ocorrer uma falha transitória, a estratégia de execução invocará o delegado novamente.


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

Essa abordagem também pode ser usada com transações ambientais.


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

Falha na confirmação da transação e o problema da idempotência

Em geral, quando há uma falha de conexão, a transação atual é revertida. No entanto, se a conexão for interrompida enquanto a transação está sendo confirmada, o estado resultante da transação é desconhecido.

Por padrão, a estratégia de execução tentará novamente a operação como se a transação tivesse sido revertida, mas se não for o caso, isso resultará em uma exceção se o novo estado do banco de dados for incompatível ou puder levar a corrupção de dados se a operação não depender de um estado específico, por exemplo, ao inserir uma nova linha com valores de chave gerados automaticamente.

Existem várias maneiras de lidar com isso.

Opção 1 - Não fazer (quase) nada

A probabilidade de uma falha de conexão durante a confirmação da transação é baixa, portanto, pode ser aceitável que seu aplicativo simplesmente falhe se essa condição realmente ocorrer.

No entanto, é necessário evitar o uso de chaves geradas pela base de dados para garantir que uma exceção seja lançada em vez de adicionar uma linha duplicada. Considere o uso de um valor GUID gerado pelo cliente ou um gerador de valor do lado do cliente.

Opção 2 - Reconstruir o estado do aplicativo

  1. Elimine o DbContextatual.
  2. Crie um novo DbContext e restaure o estado do seu aplicativo a partir do banco de dados.
  3. Informe o usuário de que a última operação pode não ter sido concluída com êxito.

Opção 3 - Adicionar verificação de estado

Para a maioria das operações que alteram o estado do banco de dados, é possível adicionar código que verifica se ele foi bem-sucedido. EF fornece um método de extensão para tornar isso mais fácil - IExecutionStrategy.ExecuteInTransaction.

Esse método começa e confirma uma transação e também aceita uma função no parâmetro verifySucceeded que é invocada quando ocorre um erro transitório durante a confirmação da transação.


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

Observação

Aqui, invoca-se SaveChanges com acceptAllChangesOnSuccess definido como false para evitar mudar o estado da entidade Blog para Unchanged se SaveChanges tiver sucesso. Isso permite repetir a mesma operação se a confirmação falhar e a transação for revertida.

Opção 4 - Rastrear manualmente a transação

Se você precisar usar chaves geradas pelo armazenamento ou precisar de uma maneira genérica de lidar com falhas de confirmação que não dependa da operação executada, cada transação poderá receber uma ID que será verificada quando a confirmação falhar.

  1. Adicione uma tabela ao banco de dados usado para acompanhar o status das transações.
  2. Insira uma linha na tabela no início de cada transação.
  3. Se a conexão falhar durante a confirmação, verifique a presença da linha correspondente no banco de dados.
  4. Se a confirmação for bem-sucedida, exclua a linha correspondente para evitar o crescimento da tabela.

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

Observação

Certifique-se de que o contexto usado para a verificação tenha uma estratégia de execução definida, pois é provável que a conexão falhe novamente durante a verificação se falhar durante a confirmação da transação.

Recursos adicionais