Является ли EnableRetryOnFailure() допустимым способом решения взаимоблокировок базы данных? Оказывает ли это негативное влияние на производительность?

#c# #sql-server #.net-core #entity-framework-core

#c# #sql-server #.net-core #entity-framework-core

Вопрос:

Итак, я столкнулся с проблемой взаимоблокировок, я получал это исключение:

System.InvalidOperationException: An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure()' to the 'UseSqlServer' call.

Я даже сделал второй проект, чтобы написать простейший код, который я могу, который приводит к той же проблеме, всего в нескольких строках кода. Это действительно базово, вам просто нужно иметь две сущности, которые ссылаются на одну и ту же различную сущность. На мой взгляд, это такая распространенная вещь, и EF Core не может справиться с этим по умолчанию и подсказывает использовать EnableRetryOnFailure() .

Я думаю, что довольно часто два или более объекта имеют отношение к одному и тому же объекту. Мы могли бы даже иметь что-то вроде системы ведения журнала в нашем приложении, которая добавляла бы запись в таблицу журнала в базе данных всякий раз, когда кто-то добавляет данные, и данные всегда были бы связаны с некоторой записью журнала. Или мы могли бы сохранить, какой пользователь добавил некоторые данные и т.д. возможности безграничны.

Мой вопрос: если у нас большое приложение, то 1000 пользователей могут запускать повторную попытку каждую секунду, в зависимости от того, как часто они используют какое-либо действие, которое приводит к этой взаимоблокировке. Не влияет ли это на производительность? Не приведет ли это в конечном итоге к засорению базы данных?

Простой код для воссоздания этой проблемы:

Это просто API со всем кодом внутри контроллеров для простоты. База данных генерируется ядром EF (подход code first). У него есть два контроллера с конечными точками, которые принимают файл json с данными, добавляемыми в базу данных. Информация о файле сохраняется каждый раз, когда данные из файла добавляются в базу данных (имя файла, дата и время). Публикация одного файла приводит к появлению множества записей с данными (в зависимости от того, что находится внутри) и одной записи с именем файла, датой и временем (каждая запись данных получает одну и ту же ссылку на файл, поэтому все записи будут иметь один и тот же InputFileId).

Вам просто нужно отправлять запросы на обе конечные точки одновременно. Или просто отправьте, например, 4 запроса одновременно на одну и ту же конечную точку. У меня было 2000 записей в файле json, поэтому потребовалось бы немного больше времени, чтобы легче вызвать взаимоблокировку.

Пользовательский контроллер:

 [Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    public MyDbContext DbContext { get; }
    public UsersController(MyDbContext dbContext)
    {
        DbContext = dbContext;
    }

    [HttpPost]
    public async Task<IActionResult> Import([FromForm]IFormFile file)
    {
        using var streamReader = new StreamReader(file.OpenReadStream());
        JsonSerializer serializer = new JsonSerializer();

        List<User> users = (List<User>)serializer.Deserialize(streamReader, typeof(List<User>));
        var inputFile = new InputFile
        {
            FileName = file.FileName,
            DateAdded = DateTime.Now
        };

        foreach (var user in users)
        {
            user.InputFile = inputFile;
        }

        await DbContext.AddRangeAsync(users);
        await DbContext.SaveChangesAsync();

        return Ok();
    }
}
  

Product Controller:

 [Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    public MyDbContext DbContext { get; }
    public ProductsController(MyDbContext dbContext)
    {
        DbContext = dbContext;
    }

    [HttpPost]
    public async Task<IActionResult> Import([FromForm]IFormFile file)
    {
        using var streamReader = new StreamReader(file.OpenReadStream());
        JsonSerializer serializer = new JsonSerializer();

        List<Product> products = (List<Product>)serializer.Deserialize(streamReader, typeof(List<Product>));
        var inputFile = new InputFile
        {
            FileName = file.FileName,
            DateAdded = DateTime.Now
        };

        foreach (var product in products)
        {
            product.InputFile = inputFile;
        }

        await DbContext.AddRangeAsync(products);
        await DbContext.SaveChangesAsync();

        return Ok();
    }
}
  

Контекст базы данных:

 public class MyDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<User> Users { get; set; }
    public DbSet<InputFile> InputFiles { get; set; }

    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    {
    }
}
  

Продукт:

 public class Product
{
    public long Id { get; set; }
    public string SerialNumber { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public long InputFileId { get; set; }
    public InputFile InputFile { get; set; }
}
  

Пользователь:

 public class User
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
    public long InputFileId { get; set; }
    public InputFile InputFile { get; set; }
}
  

Входной файл:

 public class InputFile
{
    public long Id { get; set; }
    public string FileName { get; set; }
    public DateTime DateAdded { get; set; }
}
  

Startup.cs, ConfigureServices:

 public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddDbContext<MyDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
  

Ответ №1:

Не приведет ли это в конечном итоге к засорению базы данных?

EnableRetryOnFailure Реализация по умолчанию увеличивает время ожидания между повторными попытками. Поэтому обычно это работает из коробки, даже не требуя от вас настройки.

Не влияет ли это на производительность?

Нет, это не так.

Если у нас большое приложение, то 1000 пользователей могут запускать повторную попытку каждую секунду, в зависимости от того, как часто они используют какое-либо действие, которое приводит к этой взаимоблокировке.

Также убедитесь, что вы READ_COMMITTED_SNAPSHOT установили ON .


Для получения дополнительной информации см. Отказоустойчивость соединения. Также взгляните на реализации SqlServerRetryingExecutionStrategy и ExecutionStrategy.

Комментарии:

1. Что означает «READ_COMMITTED_SNAPSHOT установлен в ON»?

2. Можно ли создать дубликаты, используя опцию EnableRetryOnFailure?

3. @AkmalSalikhov Для INSERT операций, это, вероятно, возможно в редких случаях, если вы не используете более высокий уровень транзакций. На практике для большинства приложений, если это не что-то серьезное, например банковское приложение, это обычно не имеет значения (и может даже не произойти вообще), если только соединение с сервером базы данных действительно плохое. Для тех редких приложений, где это действительно имеет значение, вы хотите протестировать эти примеры, чтобы быть уверенным. Если вам нужно, вы всегда можете пройти лишнюю милю и проверить при повторной попытке, удалось ли это предыдущему INSERT или нет.

4. @AkmalSalikhov Для получения дополнительной информации о повторных попытках и транзакциях см. Отказоустойчивость соединения: стратегии выполнения и транзакции в официальных документах.

5. @AkmalSalikhov Для получения информации о том, как справиться с конкретным случаем, который я изложил в своем первом комментарии (это единственный случай, который в редких случаях может привести к дублированию записей), см. Отказоустойчивость соединения: ошибка фиксации транзакции и проблема с идемпотентностью в той же статье официальных документов.