1.12.3 弹性链接

优质
小牛编辑
128浏览
2023-12-01

弹性链接会在数据库命令失败时自动重试。通过提供封装了故障检测和命令重试所需逻辑的“执行策略”,该功能可以应用于任何数据库。EF Core 提供程序能够根据特定的数据库故障条件和最优重试策略来提供执行策略。

比如说,SQL Server 提供程序包含一个特定的针对 SQL Server(包括 SQL Azure)的执行策略。它很清楚可以被重试的异常类型、具有合理的默认最大尝试次数、合理的两次重试之间的默认延迟等等。

在为上下文实例配置相关选项的时候就可以指定一个执行策略。这通常是在你派生上下文的 OnConfiguring 方法中完成,对于 ASP.NET Core 应用程序则在 Startup.cs 中完成。

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

自定义执行策略

这里提供了自定义执行策略的注册机制,如果你想要更改默认的策略,就可以使用它。

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

执行策略与事务

发生错误时自动重试的策略要能够在失败的重试业务块中回放每一个操作。当启用重试时,通过 EF Core 执行的每个操作自身也会成为可重试的操作,也就是说,当发生瞬时错误时每个查询以及每各 SaveChanges() 调用都将被作为一个单元被重试。

然而,如果你的代码使用 BeginTransaction() 初始化了事务,就意味着你自己定义了要被作为一个单元的一组操作,也就是说发生错误时事务中的一切都需要被回放。如果你在使用了执行策略的情况下这样做,你将收到如下异常信息。

InvaliOperationException:The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user initiated transactions. Use the execution strategy returned by 'DbContext.Database.CreateExecutionStrategy()' to execute all the operations in the transaction as a retriable unit.

大概意思就是配置的执行策略 "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();
            }
        }
    });
}

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

通常在出现连接错误时当前事务就会回滚。然而,事务提交时连接失败是无法知道事务的结果状态的。详细信息请参阅 blog post

默认情况下,执行策略将会根据事务是否回滚而重新尝试操作。但是,如果新数据库状态不兼容就会导致异常;而如果操作不依赖于某个特定状态就可能导致 数据损坏,比如说使用自动生成主键值的方式插入新的数据行。

有一些方法可以处理这种情况。

选项1 - (几乎)不做任何处理

在事务提交过程中出现连接失败的可能性很低,因此,如果发生该情况,仅仅让应用程序失败是可接受的。

但是,你需要避免使用存储方生成主键值的方式,如此以确保能够抛出异常,而不是添加重复行。建议使用客户端生成的 GUID 值或者客户端值生成器。

选项3 - 重建应用程序状态

  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。

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

注意

请确保用于验证的上下文实例定义了执行策略,因为其可能在验证期间再次连接失败(如果其在事务提交过程中失败)。