Устойчивость подключений

Устойчивость подключения автоматически повторяет неудачные команды базы данных. Эту функцию можно использовать с любой базой данных, предоставив "стратегию выполнения", которая инкапсулирует логику, необходимую для обнаружения сбоев и повторных команд. Поставщики 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",
            options => options.EnableRetryOnFailure());
}

или для Startup.cs приложения ASP.NET Core:

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 избежать изменения состояния сущности UnchangedSaveChanges в случае успешного Blog выполнения. Это позволяет повторить ту же операцию, если фиксация завершается ошибкой, и транзакция откатится.

Вариант 4. Отслеживание транзакции вручную

Если вам нужно использовать ключи, созданные в хранилище, или требуется универсальный способ обработки сбоев фиксации, которые не зависят от операции, выполняемой каждой транзакции, может быть назначен идентификатор, который проверяется при сбое фиксации.

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

Примечание

Убедитесь, что контекст, используемый для проверки, имеет стратегию выполнения, определенную как соединение, скорее всего, завершится ошибкой во время проверки, если она завершилась сбоем во время фиксации транзакции.

Дополнительные ресурсы