连接复原

连接复原能力会自动重试失败的数据库命令。 通过提供“执行策略”(封装了检测故障和重试命令所需的逻辑),此功能可用于任何数据库。 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 执行的每个操作都将成为其各自的可重试操作。 也就是说,如果发生暂时性故障,每个查询和对 SaveChanges() 的每次调用都会作为一个单元进行重试。

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

InvalidOperationException: 已配置的执行策略“SqlServerRetryingExecutionStrategy”不支持用户启动的事务。 使用由“DbContext.Database.CreateExecutionStrategy()”返回的执行策略执行事务(作为一个可回溯单元)中的所有操作。

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


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context = new BloggingContext();
        using var transaction = context.Database.BeginTransaction();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
        context.SaveChanges();

        transaction.Commit();
    });

这种方法也可用于环境事务。


using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });

var strategy = context1.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

        context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context2.SaveChanges();

        context1.SaveChanges();

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

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));

db.ChangeTracker.AcceptAllChanges();

注意

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

选项 4 - 手动跟踪事务

如果需要使用存储生成的键,或者需要使用一种通用的方法来处理提交失败,并且该方法不依赖于所执行的操作,你可以为每个事务分配一个 ID,并在提交失败时检查该 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);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
db.SaveChanges();

注意

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

其他资源