Compartilhar via


Resiliência de conexão

A resiliência de conexão repete automaticamente 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 detectar falhas e repetir comandos. Os provedores EF Core podem fornecer estratégias de execução adaptadas às condições de falha de banco de dados específicas e às políticas de repetição ideais.

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

Uma estratégia de execução é especificada ao configurar as opções para o contexto. Isso normalmente está no método OnConfiguring do 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()));
}

Nota

A habilitação da 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 conjuntos de resultados grandes. Consulte buffer 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ê quiser alterar qualquer um dos padrões.

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

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

Uma estratégia de execução que tenta novamente automaticamente 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 por meio do EF Core torna-se sua própria operação repetível. Ou seja, cada consulta e cada chamada para SaveChangesAsync() serão repetidas como uma unidade caso ocorra uma falha transitória.

No entanto, se o código iniciar uma transação usando BeginTransactionAsync() você estiver definindo seu próprio grupo de operações que precisam ser tratadas como uma unidade, e tudo dentro da transação precisará 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 dá suporte a 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 retriável.

A solução é invocar manualmente a estratégia de execução com um delegado que representa 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 de ambiente.


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 de confirmação de transação e o problema de idempotência

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

Por padrão, a estratégia de execução repetirá 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 dados corrompidos 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.

Há 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 de transação é baixa, portanto, pode ser aceitável que seu aplicativo falhe apenas se essa condição realmente ocorrer.

No entanto, você precisa evitar o uso de chaves geradas pelo repositório para garantir que uma exceção seja lançada em vez de adicionar uma linha duplicada. Considere usar um valor GUID gerado pelo cliente ou um gerador de valor do lado do cliente.

Opção 2 – Recompilar o estado do aplicativo

  1. Descarte o DbContext atual.
  2. Crie um novo DbContext e restaure o estado do aplicativo do banco de dados.
  3. Informe ao usuário 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 um código que verifica se ele foi bem-sucedido. O EF fornece um método de extensão para facilitar isso – IExecutionStrategy.ExecuteInTransaction.

Esse método inicia e confirma uma transação e também aceita uma função no parâmetro verifySucceeded que é invocado 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();

Nota

Aqui SaveChanges é invocado com acceptAllChangesOnSuccess definido como false para evitar alterar o estado da entidade Blog para Unchanged se SaveChanges tiver êxito. Isso permite repetir a mesma operação se a confirmação falhar e a transação for revertida.

Opção 4 – Acompanhar manualmente a transação

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

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

Nota

Verifique se o contexto usado para a verificação tem uma estratégia de execução definida, pois a conexão provavelmente falhará novamente durante a verificação se ela falhou durante a confirmação da transação.

Recursos adicionais