连接复原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.cs对于 ASP.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.

默认情况下,执行策略将重试该操作,就像事务已回滚一样,但如果不是这样,则这会导致在新数据库状态不兼容时导致异常,或者如果操作不依赖于特定状态(例如,使用自动生成的键值插入新行时),可能会导致 数据损坏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 ,以避免在成功时将实体的状态更改 BlogUnchanged SaveChangesHere 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.

其他资源Additional resources