连接复原Connection Resiliency

连接复原将自动重试已失败的数据库命令。 通过提供“执行策略”,它封装检测故障,然后重试命令所需的逻辑,该功能可以应用于任何数据库。 EF Core 提供程序针对特定数据库的失败条件提供定制的执行策略,同时提供最佳的重试策略。

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

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

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

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

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddDbContext<PicnicContext>(
  4. options => options.UseSqlServer(
  5. "<connection string>",
  6. providerOptions => providerOptions.EnableRetryOnFailure()));
  7. }

自定义执行策略Custom execution strategy

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

  1. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  2. {
  3. optionsBuilder
  4. .UseMyProvider(
  5. "<connection string>",
  6. options => options.ExecutionStrategy(...));
  7. }

执行策略和事务Execution strategies and transactions

在出现故障时自动重试的执行策略需要能够回滚失败的重试块中的每个操作。 启用重试后,通过 EF Core 执行的每个操作都将成为其自身的可重试操作。 也就是说,如果发生暂时性故障,每个查询和对 SaveChanges() 的每个调用都将作为一个单元重试。

但是,如果您的代码使用来启动事务 BeginTransaction() 您要定义自己的需要被视为一个单元的一组操作,则需要播放该事务中的所有内容。 如果尝试在使用执行策略时执行此操作,将收到如下所示的异常:

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

解决方法是使用代表需要执行的所有内容的委托来手动调用执行策略。 如果出现暂时性失败,执行策略将再次调用委托。

  1. using (var db = new BloggingContext())
  2. {
  3. var strategy = db.Database.CreateExecutionStrategy();
  4. strategy.Execute(() =>
  5. {
  6. using (var context = new BloggingContext())
  7. {
  8. using (var transaction = context.Database.BeginTransaction())
  9. {
  10. context.Blogs.Add(new Blog {Url = "http://blogs.msdn.com/dotnet"});
  11. context.SaveChanges();
  12. context.Blogs.Add(new Blog {Url = "http://blogs.msdn.com/visualstudio"});
  13. context.SaveChanges();
  14. transaction.Commit();
  15. }
  16. }
  17. });
  18. }

此方法还可用于环境事务。

  1. using (var context1 = new BloggingContext())
  2. {
  3. context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
  4. var strategy = context1.Database.CreateExecutionStrategy();
  5. strategy.Execute(() =>
  6. {
  7. using (var context2 = new BloggingContext())
  8. {
  9. using (var transaction = new TransactionScope())
  10. {
  11. context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
  12. context2.SaveChanges();
  13. context1.SaveChanges();
  14. transaction.Complete();
  15. }
  16. }
  17. });
  18. }

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

通常,如果连接失败,当前事务将回滚。 但是,如果在提交事务时断开连接,则事务的生成状态是未知的。

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

可以通过多种方式来处理此情况。

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

事务提交期间连接失败的可能性很低,因此如果实际发生此情况,应用程序可能会失败。

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

选项 2-重建应用程序状态Option 2 - Rebuild application state

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

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

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

此方法将启动并提交事务,并且还接受 verifySucceeded 参数中的一个函数,该参数会在事务提交期间发生暂时性错误时调用。

  1. using (var db = new BloggingContext())
  2. {
  3. var strategy = db.Database.CreateExecutionStrategy();
  4. var blogToAdd = new Blog {Url = "http://blogs.msdn.com/dotnet"};
  5. db.Blogs.Add(blogToAdd);
  6. strategy.ExecuteInTransaction(db,
  7. operation: context =>
  8. {
  9. context.SaveChanges(acceptAllChangesOnSuccess: false);
  10. },
  11. verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));
  12. db.ChangeTracker.AcceptAllChanges();
  13. }

备注

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

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

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

  1. 向数据库添加一个用于跟踪事务状态的表。
  2. 在每个事务开头的表中插入一行。
  3. 如果在提交期间连接失败,请检查数据库中是否存在相应的行。
  4. 如果提交成功,则删除相应的行以避免表增长。
  1. using (var db = new BloggingContext())
  2. {
  3. var strategy = db.Database.CreateExecutionStrategy();
  4. db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
  5. var transaction = new TransactionRow {Id = Guid.NewGuid()};
  6. db.Transactions.Add(transaction);
  7. strategy.ExecuteInTransaction(db,
  8. operation: context =>
  9. {
  10. context.SaveChanges(acceptAllChangesOnSuccess: false);
  11. },
  12. verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));
  13. db.ChangeTracker.AcceptAllChanges();
  14. db.Transactions.Remove(transaction);
  15. db.SaveChanges();
  16. }

备注

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