Устойчивость подключения
Механизм восстановления подключения автоматически повторяет неудавшиеся команды базы данных. Эту функцию можно использовать с любой базой данных, указав "стратегию выполнения", которая инкапсулирует логику, необходимую для обнаружения сбоев и повторных команд. Поставщики 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;ConnectRetryCount=0",
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, становится собственной повторной операцией. То есть каждый запрос и каждый вызов SaveChangesAsync()
будут повторно выполнены как единое целое, если произойдет временный сбой.
Однако, если ваш код инициирует транзакцию, используя BeginTransactionAsync()
, вы определяете свою собственную группу операций, которые необходимо рассматривать как единое целое, и в случае сбоя всё внутри транзакции потребуется выполнить заново. При использовании стратегии выполнения вы можете столкнуться с исключением, подобным следующему.
InvalidOperationException: настроенная стратегия выполнения "SqlServerRetryingExecutionStrategy" не поддерживает транзакции, инициированные пользователем. Используйте стратегию выполнения, возвращаемую dbContext.Database.CreateExecutionStrategy(), для выполнения всех операций в транзакции в качестве возвращаемой единицы.
Решение состоит в том, чтобы вручную вызвать стратегию выполнения с делегатом, представляющим все, что необходимо выполнить. Если происходит временный сбой, стратегия выполнения снова вызовет делегат.
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(
async () =>
{
using var context = new BloggingContext();
await using var transaction = await context.Database.BeginTransactionAsync();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context.SaveChangesAsync();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
await context.SaveChangesAsync();
await transaction.CommitAsync();
});
Этот подход также можно использовать с внешними транзакциями.
using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
var strategy = context1.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(
async () =>
{
using var context2 = new BloggingContext();
using var transaction = new TransactionScope();
context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context2.SaveChangesAsync();
await context1.SaveChangesAsync();
transaction.Complete();
});
Сбой подтверждения транзакций и проблема с идемпотентностью
Как правило, при сбое подключения текущая транзакция откатывается. Тем не менее, если подключение удаляется во время фиксации транзакции, итоговое состояние транзакции неизвестно.
По умолчанию стратегия выполнения повторит операцию, как если бы транзакция была успешно откатена, однако, если это не произойдет, это может привести к исключению, если новое состояние базы данных несовместимо с существующим или может привести к повреждению данных, если операция не зависит от определенного состояния, например, при вставке новой строки с автоматически созданными ключевыми значениями.
Существует несколько способов справиться с этим.
Вариант 1. Делать (почти) ничего
Вероятность сбоя подключения во время фиксации транзакции низка, поэтому для приложения может быть приемлемо просто завершить работу с ошибкой, если это условие действительно произойдет.
Однако необходимо избежать использования ключей, созданных системой, чтобы гарантировать, что будет выброшено исключение, а не добавлена повторяющаяся строка. Рекомендуется использовать созданное клиентом значение GUID или генератор значений на стороне клиента.
Вариант 2. Перестроение состояния приложения
- Удалить текущий
DbContext
. - Создайте новую
DbContext
и восстановите состояние приложения из базы данных. - Сообщите пользователю, что последняя операция не была выполнена успешно.
Вариант 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);
await strategy.ExecuteInTransactionAsync(
db,
operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
verifySucceeded: (context, cancellationToken) => context.Blogs.AsNoTracking().AnyAsync(b => b.BlogId == blogToAdd.BlogId, cancellationToken));
db.ChangeTracker.AcceptAllChanges();
Заметка
Здесь SaveChanges
вызывается с установкой acceptAllChangesOnSuccess
в false
, чтобы избежать изменения состояния сущности Blog
на Unchanged
, если SaveChanges
выполняется успешно. Это позволяет повторить ту же операцию, если фиксация завершается ошибкой, и транзакция откатывается.
Вариант 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);
await strategy.ExecuteInTransactionAsync(
db,
operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
verifySucceeded: (context, cancellationToken) => context.Transactions.AsNoTracking().AnyAsync(t => t.Id == transaction.Id, cancellationToken));
db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
await db.SaveChangesAsync();
Заметка
Убедитесь, что для контекста, используемого в проверочной проверке, определена стратегия выполнения, иначе подключение, скорее всего, снова завершится ошибкой во время проверки, если оно завершилось ошибкой во время фиксации транзакции.