Compartir a través de


Resistencia de conexión

La resiliencia de la conexión reintenta automáticamente los comandos de base de datos fallidos. La característica se puede usar con cualquier base de datos proporcionando una "estrategia de ejecución", que encapsula la lógica necesaria para detectar errores y volver a intentar comandos. Los proveedores de EF Core pueden proporcionar estrategias de ejecución adaptadas a sus condiciones de error de base de datos específicas y directivas de reintento óptimas.

Por ejemplo, el proveedor de SQL Server incluye una estrategia de ejecución que se adapta específicamente a SQL Server (incluido SQL Azure). Es consciente de los tipos de excepción que pueden ser reintentados y tiene configuraciones predeterminadas sensatas para el número máximo de reintentos, interrupción entre los mismos, etc.

Se especifica una estrategia de ejecución al configurar las opciones del contexto. Normalmente, esto se encuentra en el método OnConfiguring del contexto derivado:

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

o en Startup.cs para una aplicación ASP.NET Core:

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

Nota

Habilitar el reintento en caso de error hace que EF almacene internamente en búfer el conjunto de resultados, lo que puede aumentar significativamente los requisitos de memoria para las consultas que devuelven grandes conjuntos de resultados. Para más información, consulte Almacenamiento en búfer y streaming.

Estrategia de ejecución personalizada

Hay un mecanismo para registrar una estrategia de ejecución personalizada propia si desea cambiar cualquiera de los valores predeterminados.

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

Estrategias y transacciones de ejecución

Una estrategia de ejecución que reintente automáticamente en caso de error debe ser capaz de reproducir cada operación de un bloque de reintento que genere un error. Cuando se habilitan los reintentos, cada operación que realice a través de EF Core se convierte en una operación que se puede volver a intentar por sí misma. Es decir, cada consulta y cada llamada a SaveChangesAsync() se reintentará como una unidad si se produce un error transitorio.

Sin embargo, si el código inicia una transacción con BeginTransactionAsync(), va a definir su propio grupo de operaciones que se deben tratar como una unidad, y habría que reproducir todo dentro de la transacción si se produce un error. Recibirá una excepción similar a la siguiente si intenta hacerlo al usar una estrategia de ejecución:

InvalidOperationException: la estrategia de ejecución configurada "SqlServerRetryingExecutionStrategy" no admite transacciones iniciadas por el usuario. Use la estrategia de ejecución devuelta por "DbContext.Database.CreateExecutionStrategy()" para ejecutar todas las operaciones de la transacción como una unidad reintable.

La solución consiste en invocar manualmente la estrategia de ejecución con un delegado que representa todo lo que debe ejecutarse. Si se produce un error transitorio, la estrategia de ejecución invocará al delegado de nuevo.


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

Este enfoque también se puede usar con transacciones ambientales.


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

Error de confirmación de transacción y problema de idempotencia

En general, cuando se produce un error de conexión, la transacción actual se revierte. Sin embargo, si se quita la conexión mientras la transacción se confirma, se desconoce el estado resultante de la transacción.

De forma predeterminada, la estrategia de ejecución reintentará la operación como si la transacción se revierta, pero si no es así, se producirá una excepción si el nuevo estado de la base de datos no es compatible o podría provocar daños en los datos si la operación no se basa en un estado determinado, por ejemplo, al insertar una nueva fila con valores de clave generados automáticamente.

Hay varias maneras de tratar esto.

Opción 1: Hacer (casi) nada

La probabilidad de un error de conexión durante la confirmación de transacción es baja, por lo que puede ser aceptable que la aplicación solo produzca un error si se produce realmente esta condición.

Sin embargo, debe evitar el uso de claves generadas por el almacén para asegurarse de que se produce una excepción en lugar de agregar una fila duplicada. Considere la posibilidad de usar un valor GUID generado por el cliente o un generador de valores del lado cliente.

Opción 2: Volver a generar el estado de la aplicación

  1. Descarte el valor DbContext actual.
  2. Cree una nueva DbContext y restaure el estado de la aplicación desde la base de datos.
  3. Informe al usuario de que es posible que la última operación no se haya completado correctamente.

Opción 3: Agregar comprobación de estado

Para la mayoría de las operaciones que cambian el estado de la base de datos, es posible agregar código que compruebe si se realizó correctamente. EF proporciona un método de extensión para que sea más fácil: IExecutionStrategy.ExecuteInTransaction.

Este método comienza y confirma una transacción y también acepta una función en el parámetro verifySucceeded que se invoca cuando se produce un error transitorio durante la confirmación de la transacción.


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

Aquí, SaveChanges se invoca con acceptAllChangesOnSuccess establecido en false para evitar cambiar el estado de la entidad Blog a Unchanged si SaveChanges se completa correctamente. Esto permite reintentar la misma operación si se produce un error en la confirmación y se revierte la transacción.

Opción 4: seguimiento manual de la transacción

Si necesita usar claves generadas por el almacén o necesita una forma genérica de controlar los errores de confirmación que no dependen de la operación realizada, se podría asignar a cada transacción un identificador que se comprueba cuando se produce un error en la confirmación.

  1. Agregue una tabla a la base de datos utilizada para realizar un seguimiento del estado de las transacciones.
  2. Inserte una fila en la tabla al principio de cada transacción.
  3. Si se produce un error en la conexión durante la confirmación, compruebe la presencia de la fila correspondiente en la base de datos.
  4. Si la confirmación se realiza correctamente, elimine la fila correspondiente para evitar el crecimiento de la tabla.

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

Asegúrese de que el contexto utilizado para la verificación tiene definida una estrategia de ejecución, ya que la conexión probablemente falle de nuevo durante la verificación si falló durante la confirmación de la transacción.

Recursos adicionales