Ответ
Оптимистическая блокировка (Optimistic Concurrency Control) — это стратегия управления параллельным доступом к данным, которая предполагает, что конфликты между транзакциями редки. Вместо блокировки строк на время чтения, она проверяет, не изменились ли данные с момента их чтения, непосредственно перед записью.
Принцип работы:
- Чтение: Клиент читает запись, получая ее текущее состояние и версию (например,
Version= 5). - Модификация: Клиент изменяет данные локально.
- Запись: При отправке обновления клиент включает прочитанную версию (
Version = 5). - Проверка: Система обновляет запись только если ее текущая версия в БД все еще равна 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+ 🔞
А, оптимистическая блокировка, ну это же классика, блядь! Представь себе: ты такой заходишь в бар, видишь на стойке последний круассан. Берёшь его в руки, смотришь — свежий, вроде никто не трогал. Идёшь к кассе, а там очередь дохуя. Ты стоишь, держишь этот круассан, мечтаешь о нём. Подходишь наконец, а кассирша тебе: «Извините, мужик, только что последний забрали». Вот это и есть конфликт версий, ёпта! Ты думал, что он твой, а пока ты в очереди стоял, его уже кто-то хуяк и съел.
Так вот, оптимистическая блокировка — это когда мы надеемся, что все адекватные и конфликтов не будет. Мы не вешаем замок на круассан, пока его разглядываем. Мы просто запоминаем, как он выглядел (версию). А когда уже платить, проверяем — а не поменялся ли он? Не надкусил ли его кто? Если всё ок — забираем. Если нет — получаем по ебалу в виде исключения.
Как это работает, если по-простому:
- Читаем запись. Запоминаем, какая у неё версия. Допустим,
Version = 5. Как серийный номер на круассане. - Ковыряем её у себя в коде. Меняем цену, название — что угодно.
- Пытаемся сохранить. Говорим базе: «Слушай, дай-ка обновить запись с id=10, но ТОЛЬКО если её версия до сих пор 5, окей? И заодно версию увеличь до 6».
- Ждём ответа. Если за это время другой чувак уже успел обновить эту запись (и версия стала 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и вешать замок на запись, пока не закончил. Чтение медленнее, зато ты уверен, что тебя не обгонят.
Короче, если не хочешь, чтобы твои данные ебались как кошки в марте, выбирай стратегию с умом. Оптимистичная — для вежливых и неторопливых. Пессимистичная — для жадных и быстрых.