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

«Что такое оптимистическая блокировка и как её реализовать?» — вопрос из категории Базы данных, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Оптимистическая блокировка (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.