Что такое оптимистическая блокировка и как её реализовать?

Ответ

Оптимистическая блокировка (Optimistic Concurrency Control) — это стратегия управления параллельным доступом к данным, которая предполагает, что конфликты между транзакциями редки. Вместо блокировки строк на время чтения, она проверяет, не изменились ли данные с момента их чтения, непосредственно перед записью.

Принцип работы:

  1. Чтение: Клиент читает запись, получая ее текущее состояние и версию (например, Version = 5).
  2. Модификация: Клиент изменяет данные локально.
  3. Запись: При отправке обновления клиент включает прочитанную версию (Version = 5).
  4. Проверка: Система обновляет запись только если ее текущая версия в БД все еще равна 5, и одновременно увеличивает версию (до 6). Если версия изменилась (другой клиент уже обновил запись), операция завершается ошибкой (DbUpdateConcurrencyException в EF Core).

Реализация:

1. Добавление столбца версии в сущность

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    // Свойство для оптимистической блокировки
    [Timestamp] // Для SQL Server (тип rowversion)
    public byte[] RowVersion { get; set; }
    // ИЛИ для других БД (int версия)
    // public int Version { get; set; }
}

2. Обработка в коде с использованием EF Core

try
{
    var product = await _context.Products.FindAsync(productId);
    product.Price = newPrice; // Изменяем данные
    // EF Core автоматически проверит RowVersion/Version при SaveChanges
    await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    // Конфликт! Данные были изменены кем-то другим.
    foreach (var entry in ex.Entries)
    {
        var databaseValues = await entry.GetDatabaseValuesAsync();
        if (databaseValues == null)
        {
            Console.WriteLine("Запись была удалена другим пользователем.");
        }
        else
        {
            // Получаем актуальные значения из БД
            var currentValues = entry.CurrentValues;
            var dbProduct = (Product)databaseValues.ToObject();
            // Стратегия разрешения конфликта:
            // 1. Перезаписать клиентскими данными (Client Wins) - entry.OriginalValues.SetValues(databaseValues); затем повторить.
            // 2. Принять серверные данные (Store Wins) - entry.CurrentValues.SetValues(databaseValues);
            // 3. Объединить изменения вручную.
            Console.WriteLine($"Конфликт! Текущая цена в БД: {dbProduct.Price}");
        }
    }
}

3. Ручная проверка через WHERE-условие (SQL)

UPDATE Products
SET Price = @NewPrice, Version = Version + 1
WHERE Id = @ProductId AND Version = @OriginalVersion;
-- Проверяем количество затронутых строк. Если 0 — значит, версия изменилась.

Когда использовать:

  • Оптимистическая: Для сценариев с низкой вероятностью конфликтов (редактирование профилей, каталогов товаров). Высокая производительность чтения.
  • Пессимистическая: Для сценариев с высокой конкуренцией (бухгалтерские операции, бронирование последнего билета). Использует блокировки SELECT ... FOR UPDATE.

Ответ 18+ 🔞

А, оптимистическая блокировка, ну это же классика, блядь! Представь себе: ты такой заходишь в бар, видишь на стойке последний круассан. Берёшь его в руки, смотришь — свежий, вроде никто не трогал. Идёшь к кассе, а там очередь дохуя. Ты стоишь, держишь этот круассан, мечтаешь о нём. Подходишь наконец, а кассирша тебе: «Извините, мужик, только что последний забрали». Вот это и есть конфликт версий, ёпта! Ты думал, что он твой, а пока ты в очереди стоял, его уже кто-то хуяк и съел.

Так вот, оптимистическая блокировка — это когда мы надеемся, что все адекватные и конфликтов не будет. Мы не вешаем замок на круассан, пока его разглядываем. Мы просто запоминаем, как он выглядел (версию). А когда уже платить, проверяем — а не поменялся ли он? Не надкусил ли его кто? Если всё ок — забираем. Если нет — получаем по ебалу в виде исключения.

Как это работает, если по-простому:

  1. Читаем запись. Запоминаем, какая у неё версия. Допустим, Version = 5. Как серийный номер на круассане.
  2. Ковыряем её у себя в коде. Меняем цену, название — что угодно.
  3. Пытаемся сохранить. Говорим базе: «Слушай, дай-ка обновить запись с id=10, но ТОЛЬКО если её версия до сих пор 5, окей? И заодно версию увеличь до 6».
  4. Ждём ответа. Если за это время другой чувак уже успел обновить эту запись (и версия стала 6), база тебе такой: «Не, братан, опоздал. Конфликт». И шлёт тебе DbUpdateConcurrencyException. Всё, пиздец твоим изменениям.

Как это впихнуть в код:

1. Сущность. Надо добавить поле для версии.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    // Вариант для SQL Server — ставим [Timestamp], будет тип rowversion
    [Timestamp]
    public byte[] RowVersion { get; set; } // Автоматически меняется при каждом обновлении
    // Или для других баз можно просто int
    // public int Version { get; set; }
}

2. Обработка в EF Core. Вот тут вся магия и происходит. Пытаешься сохранить, а оно может и выстрелить.

try
{
    var product = await _context.Products.FindAsync(productId);
    product.Price = newPrice; // Твои дерзкие изменения
    // SaveChanges сам проверит RowVersion/Version в БД
    await _context.SaveChangesAsync();
    // Если дошло сюда — ты красава, конфликта не было.
}
catch (DbUpdateConcurrencyException ex) // А вот если поймал эту гадину...
{
    // Значит, кто-то уже успел обновить запись, пока ты тут думал.
    foreach (var entry in ex.Entries)
    {
        // Пытаемся достать то, что сейчас лежит в базе (тот самый надкушенный круассан)
        var databaseValues = await entry.GetDatabaseValuesAsync();
        if (databaseValues == null)
        {
            Console.WriteLine("Да запись-то уже удалили, о чём ты?");
        }
        else
        {
            // Вот актуальные данные из БД
            var dbProduct = (Product)databaseValues.ToObject();
            // И тут решай, страдалец, что делать:
            // 1. "Моё — моё!" (Client Wins) — перезаписать данные в БД своими.
            //    entry.OriginalValues.SetValues(databaseValues);
            //    await _context.SaveChangesAsync(); // Пробуем ещё раз
            // 2. "Ладно, сдаюсь" (Store Wins) — принимаем данные из БД.
            //    entry.CurrentValues.SetValues(databaseValues);
            // 3. Ручное мерджирование — это когда ты умный и пытаешься слить изменения.
            Console.WriteLine($"Конфликт, детка! Пока ты тупил, цена в БД стала {dbProduct.Price}");
        }
    }
}

3. Если ты мазохист и любишь SQL. Можно и руками, конечно:

UPDATE Products
SET Price = @NewPrice, Version = Version + 1
WHERE Id = @ProductId AND Version = @OriginalVersion;
-- Смотришь, сколько строк обновилось. Если ноль — значит, версия уже не та, тебя опередили.

Когда это юзать, а когда нет?

  • Оптимистическая (про версии): Когда все редактируют разное, или конфликты раз в год. Производительность чтения — огонь, блокировок нет. Идеально для всяких каталогов, профилей юзеров.
  • Пессимистическая (про блокировки): Когда идёт драка за последний ресурс. Типа «один билет на концерт» или «последняя пачка гречки». Тут сразу надо кричать SELECT ... FOR UPDATE и вешать замок на запись, пока не закончил. Чтение медленнее, зато ты уверен, что тебя не обгонят.

Короче, если не хочешь, чтобы твои данные ебались как кошки в марте, выбирай стратегию с умом. Оптимистичная — для вежливых и неторопливых. Пессимистичная — для жадных и быстрых.