连接弹性

连接的弹性机制会自动重新尝试失败的数据库命令。 该功能可以通过提供“执行策略”来与任何数据库一起使用,该策略封装了检测失败和重试命令所需的逻辑。 EF Core 提供程序可以提供针对其特定数据库故障条件和最佳重试策略定制的执行策略。

例如,SQL Server 提供程序包含专门针对 SQL Server(包括 SQL Azure)定制的执行策略。 它知道可以重试的异常类型,并具有合理的默认值,用于最大重试、重试之间的延迟等。

为上下文配置选项时,指定执行策略。 这通常是在派生上下文的 OnConfiguring 方法中:

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

或在 ASP.NET Core 应用程序的 Startup.cs 中:

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

说明

在失败时启用重试会导致 EF 在内部缓冲结果集,这可能会显著增加返回大型结果集的查询的内存要求。 有关详细信息,请参阅 缓冲和流式处理

自定义执行策略

如果想要更改任何默认值,可以使用一种机制来注册自己的自定义执行策略。

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

执行策略和交易

自动在失败时重试的执行策略必须能够播放失败的重试块中的每个操作。 启用重试后,通过 EF Core 执行的每个操作都将变为其自己的可重试操作。 也就是说,如果发生暂时性故障,每个查询和每次对 SaveChangesAsync() 的调用将作为一个整体被重试。

但是,如果代码使用 BeginTransactionAsync() 启动事务,这表示你在定义自己的一组操作,这些操作需要被视为一个单元,如果发生故障,需要播放事务内的所有内容。 如果在使用执行策略时尝试执行此操作,将收到如下异常:

无效操作异常:配置的执行策略“SqlServerRetryingExecutionStrategy”不支持用户发起的事务。 使用“DbContext.Database.CreateExecutionStrategy()”返回的执行策略将事务中的所有操作作为可重试单元执行。

解决方案是通过委托来手动调用执行策略,该委托代表需要执行的所有操作。 如果发生暂时性故障,执行策略会再次调用委托。


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

这种方法同样适用于环境交易。


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

事务提交失败和幂等性问题

通常,当连接失败时,当前事务会回滚。 但是,如果在提交事务时删除了连接,则事务的结果状态未知。

默认情况下,执行策略将重试操作,就像回滚事务一样,但如果不是这样,则如果新数据库状态不兼容或可能导致 数据损坏, 如果操作不依赖于特定状态,例如插入具有自动生成的键值的新行时,则会导致异常。

有多种方法可以解决此问题。

选项 1 - 几乎不做任何改变

事务提交期间连接失败的可能性很低,因此如果真的出现这种情况,应用程序失败也是可以接受的。

但是,你需要避免使用存储生成的键,以确保引发异常而不是添加重复行。 请考虑使用客户端生成的 GUID 值或客户端值生成器。

选项 2 - 重新生成应用程序状态

  1. 放弃当前的 DbContext
  2. 创建新的 DbContext 并从数据库还原应用程序的状态。
  3. 通知用户最后一个操作可能尚未成功完成。

选项 3 - 添加状态验证

对于更改数据库状态的大多数操作,可以添加检查它是否成功的代码。 EF 提供了一种扩展方法来简化此操作 - IExecutionStrategy.ExecuteInTransaction

此方法开始并提交事务,并接受在事务提交期间发生暂时性错误时调用的 verifySucceeded 参数中的函数。


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

说明

SaveChanges 成功的情况下,调用 SaveChanges,将 acceptAllChangesOnSuccess 设置为 false,以避免将 Blog 实体的状态更改为 Unchanged。 如果提交失败并且事务回滚,则允许重试相同的操作。

选项 4 - 手动跟踪交易

如果您需要使用存储生成的密钥,或需要一种不依赖于每笔事务操作的泛型方式来处理提交失败,则可以为每个事务分配一个 ID,并在提交失败时进行检查。

  1. 将表添加到用于跟踪事务状态的数据库。
  2. 在每个事务开始时向表中插入一行。
  3. 如果在提交期间连接失败,请检查数据库中是否存在相应的行。
  4. 如果提交成功,请删除相应的行以避免表的增长。

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

说明

确保用于验证的上下文定义了执行策略,因为如果连接曾在事务提交期间失败,它可能会在验证期间再次失败。

其他资源