Реализуем Пессимистическую Блокировку в EF Core. Начало
Иногда, особенно в сценариях с высоким трафиком, вам абсолютно необходимо убедиться, что только один процесс может одновременно изменять какую-то часть данных.
Стандартный пример - система продажи билетов на популярное событие. При большом трафике билеты на одно место могут быть куплены одновременно несколькими людьми. В EF Core нет прямого механизма пессимистичной блокировки. Оптимистическая блокировка (с использованием версий) может работать, но в сценариях с высоким уровнем конфликтов это может привести к множеству повторных попыток.
Вот упрощённый фрагмент кода, иллюстрирующий проблему с билетами:
public async Task Checkout(ShopCart cart)
{
await using var trans = await
context.BeginTransactionAsync();
…
var order = new Order();
foreach (CartItem item in cart.Items)
{
// Проверяем доступность билета
// Что, если два запроса проверят его в одно время?
var ticket = await ticketRepo.GetAsync(
item.TicketId);
ticket.UpdateQuantity(item.Quantity);
order.Add(ticket, item.Quantity, item.Price);
}
orderRepo.Insert(order);
await context.SaveChangesAsync();
await trans.CommitAsync();
…
}
Пример выше надуманный, но суть понятна. Во время оформления заказа мы проверяем доступное количество для каждого типа билетов. Что, если мы получим одновременные запросы на покупку одного и того же билета? В худшем случае мы продадим билеты несколько раз. Одновременные запросы могут увидеть доступность билета и оба завершить оформление заказа.
Поскольку EF Core не предлагает пессимистическую блокировку напрямую, немного углубимся в старый добрый SQL. Заменим вызов GetAsync для получения билета на GetWithLockAsync:
public async Task<Ticket> GetWithLockAsync(Guid id)
{
return await context
.Tickets
.FromSql(
$@"SELECT id, event_id, price, quantity
FROM tickets WHERE id = {id}
FOR UPDATE NOWAIT")
.SingleAsync();
}
FOR UPDATE NOWAIT - суть пессимистической блокировки в PostgreSQL (и Oracle). Он сообщает базе данных: «Захвати эту строку, заблокируй её для меня, и, если она уже заблокирована, прямо сейчас выдай ошибку».Вызов GetWithLockAsync нужно обернуть в try-catch, чтобы корректно обрабатывать сбои блокировки, либо повторяя попытку, либо уведомляя пользователя.
Поскольку в EF Core нет встроенного способа добавления подсказок к запросам, приходится писать чистый SQL-запрос. Мы можем использовать оператор
SELECT FOR UPDATE, чтобы получить блокировку на уровне выбранных строк. Любые конкурирующие транзакции будут заблокированы до тех пор, пока текущая транзакция не снимет блокировку. Это очень простой способ реализовать пессимистическую блокировку.Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/a-clever-way-to-implement-pessimistic-locking-in-ef-core