连接复原Connection Resiliency

连接复原将自动重试已失败的数据库命令。Connection resiliency automatically retries failed database commands. 通过提供“执行策略”,它封装检测故障,然后重试命令所需的逻辑,该功能可以应用于任何数据库。The feature can be used with any database by supplying an "execution strategy", which encapsulates the logic necessary to detect failures and retry commands. EF Core 提供程序针对特定数据库的失败条件提供定制的执行策略,同时提供最佳的重试策略。EF Core providers can supply execution strategies tailored to their specific database failure conditions and optimal retry policies.

例如,SQL Server 提供程序包括专门针对 SQL Server (包括 SQL Azure) 的执行策略。As an example, the SQL Server provider includes an execution strategy that is specifically tailored to SQL Server (including SQL Azure). 它知道可以重试的异常类型,并且具有合理的默认值的最大重试,重试次数等之间的延迟。It is aware of the exception types that can be retried and has sensible defaults for maximum retries, delay between retries, etc.

为上下文配置选项时将指定执行策略。An execution strategy is specified when configuring the options for your context. 这是通常在OnConfiguring派生上下文的方法:This is typically in the OnConfiguring method of your derived context:

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

或在Startup.csASP.NET Core 应用程序:or in Startup.cs for an ASP.NET Core application:

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

自定义执行策略Custom execution strategy

没有一种机制来注册您如果想要更改任何默认设置自己的自定义执行策略。There is a mechanism to register a custom execution strategy of your own if you wish to change any of the defaults.

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

执行策略和事务Execution strategies and transactions

在出现故障时自动重试的执行策略需要能够回滚失败的重试块中的每个操作。An execution strategy that automatically retries on failures needs to be able to play back each operation in a retry block that fails. 启用重试后,通过 EF Core 执行的每个操作都将成为其自身的可重试操作。When retries are enabled, each operation you perform via EF Core becomes its own retriable operation. 也就是说,如果出现暂时性故障,每个查询和对 SaveChanges() 的每次调用都将作为一个单元进行重试。That is, each query and each call to SaveChanges() will be retried as a unit if a transient failure occurs.

但是,如果你的代码使用 BeginTransaction() 启动事务,则你将定义自己的操作组(这些操作需要被视为一个单元),并且如果发生故障,将需要回滚事务内的所有内容。However, if your code initiates a transaction using BeginTransaction() you are defining your own group of operations that need to be treated as a unit, and everything inside the transaction would need to be played back shall a failure occur. 如果尝试在使用执行策略时执行此操作,将收到如下所示的异常:You will receive an exception like the following if you attempt to do this when using an execution strategy:

InvalidOperationException:已配置的执行策略“SqlServerRetryingExecutionStrategy”不支持用户启动的事务。InvalidOperationException: The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user initiated transactions. 使用由“DbContext.Database.CreateExecutionStrategy()”返回的执行策略执行事务(作为一个可回溯单元)中的所有操作。Use the execution strategy returned by 'DbContext.Database.CreateExecutionStrategy()' to execute all the operations in the transaction as a retriable unit.

解决方法是使用代表需要执行的所有内容的委托来手动调用执行策略。The solution is to manually invoke the execution strategy with a delegate representing everything that needs to be executed. 如果发生暂时性故障,执行策略将再次调用委托。If a transient failure occurs, the execution strategy will invoke the delegate again.

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

这种方法还可用于环境事务。This approach can also be used with ambient transactions.

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

事务提交失败和幂等性问题Transaction commit failure and the idempotency issue

一般情况下,连接失败时当前事务会回滚。In general, when there is a connection failure the current transaction is rolled back. 但是,如果在连接断开时在事务正在将提交所生成的事务状态为未知。However, if the connection is dropped while the transaction is being committed the resulting state of the transaction is unknown. 请参阅此博客文章的更多详细信息。See this blog post for more details.

默认情况下,执行策略将重试该操作,就像该事务已回滚一样。但如果未这样做,当新数据库状态不兼容时,则可能导致异常;或者当操作不依赖于特定状态时(例如使用自动生成的键值插入新行时),则可能导致数据损坏。By default, the execution strategy will retry the operation as if the transaction was rolled back, but if it's not the case this will result in an exception if the new database state is incompatible or could lead to data corruption if the operation does not rely on a particular state, for example when inserting a new row with auto-generated key values.

有几种方法来解决此问题。There are several ways to deal with this.

选项 1-执行操作 (几乎) 执行任何操作Option 1 - Do (almost) nothing

事务提交期间连接失败的可能性很低,因此如果实际发生此情况,应用程序可能会失败。The likelihood of a connection failure during transaction commit is low so it may be acceptable for your application to just fail if this condition actually occurs.

但是,需要避免使用存储生成的密钥,以确保引发异常而不是添加重复的行。However, you need to avoid using store-generated keys in order to ensure that an exception is thrown instead of adding a duplicate row. 请考虑使用客户端生成的 GUID 值或客户端值生成器。Consider using a client-generated GUID value or a client-side value generator.

选项 2-重新生成应用程序状态Option 2 - Rebuild application state

  1. 放弃当前DbContextDiscard the current DbContext.
  2. 创建一个新DbContext和从数据库中还原应用程序的状态。Create a new DbContext and restore the state of your application from the database.
  3. 通知用户上次操作可能尚未成功完成。Inform the user that the last operation might not have been completed successfully.

选项 3-添加状态验证Option 3 - Add state verification

对于大多数更改数据库状态的操作就可以添加代码来检查它是否成功。For most of the operations that change the database state it is possible to add code that checks whether it succeeded. EF 提供了一个扩展方法来简化此过程- IExecutionStrategy.ExecuteInTransactionEF provides an extension method to make this easier - IExecutionStrategy.ExecuteInTransaction.

此方法将开始并提交事务,还将接受在事务提交期间出现暂时性错误时所调用的 verifySucceeded参数中的函数。This method begins and commits a transaction and also accepts a function in the verifySucceeded parameter that is invoked when a transient error occurs during the transaction commit.

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为了避免更改的状态Blog到实体Unchanged如果SaveChanges成功。Here SaveChanges is invoked with acceptAllChangesOnSuccess set to false to avoid changing the state of the Blog entity to Unchanged if SaveChanges succeeds. 这样就可以重试相同的操作,如果提交失败并回滚事务。This allows to retry the same operation if the commit fails and the transaction is rolled back.

选项 4-手动跟踪事务Option 4 - Manually track the transaction

如果需要使用存储生成的密钥或需要一种处理提交失败且不依赖于所执行操作的通用方法,则可以为每个事务分配一个可在提交失败时进行检查的 ID。If you need to use store-generated keys or need a generic way of handling commit failures that doesn't depend on the operation performed each transaction could be assigned an ID that is checked when the commit fails.

  1. 将表添加到用于跟踪事务的状态的数据库。Add a table to the database used to track the status of the transactions.
  2. 插入到表中每个事务的开始处的行。Insert a row into the table at the beginning of each transaction.
  3. 如果连接失败在提交期间,检查存在的数据库中的相应行。If the connection fails during the commit, check for the presence of the corresponding row in the database.
  4. 如果提交成功,删除对应的行,以避免对表的增长。If the commit is successful, delete the corresponding row to avoid the growth of the table.
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();
}

备注

请确保用于验证的上下文具有定义的执行策略,因为如果在事务提交期间连接失败,则连接可能在验证期间再次失败。Make sure that the context used for the verification has an execution strategy defined as the connection is likely to fail again during verification if it failed during transaction commit.