Решение Проблем Гонки с Помощью Оптимистической Блокировки EF Core. Начало
Как часто вы думаете о конфликтах параллелизма при написании кода? Вы пишете код новой функции, проверяете, что она работает, и всё. Но неделю спустя вы обнаруживаете, что допустили неприятную ошибку, потому что не подумали о параллелизме. Наиболее распространённой проблемой являются условия гонки, когда два конкурирующих потока выполняют одну и ту же функцию. Если вы не учтёте это во время разработки, вы рискуете оставить систему в повреждённом состоянии.
Рассмотрим пример.
Бизнес-требование: нельзя иметь два перекрывающихся бронирования на одни и те же даты. В следующем примере кода скрывается проблема состояния гонки:
public Result<Guid> Handle(
ReserveBooking command,
AppDbContext ctx)
{
var user = ctx.Users.GetById(command.UserId);
var apart = ctx.Aparts.GetById(command.ApartId);
var (start, end) = command;
if (dbContext.Bookings
.IsOverlapping(apart, start, end))
return Result.Failure<Guid>(Errors.Overlap);
var booking = Booking
.Reserve(apart, user, start, end);
ctx.Add(booking);
ctx.SaveChanges();
return booking.Id;
}
Вызов IsOverlapping — это оптимистичная проверка, существует ли бронирование на указанные даты. Если ответ true, это попытка конкурентного бронирования на те же даты, поэтому мы возвращаем ошибку. Но если возвращается false, мы бронируем и вызываем SaveChanges, чтобы сохранить изменения в БД.
Проблема в том, что существует вероятность того, что параллельный запрос пройдёт проверку IsOverlapping и попытается сделать бронирование. Без какого-либо контроля параллелизма оба запроса будут успешными, и в итоге мы получим несогласованное состояние БД.
При пессимистическом подходе обработки параллелизма данные в БД блокируются между чтением и изменением. Это медленно и приводит к блокировке конкурирующих транзакций до тех пор, пока блокировка не будет снята. EF Core не поддерживает этот подход «из коробки». Оптимистический параллелизм в EF Core не требует блокировок, но любые изменения данных не будут сохранены, если данные изменились с момента чтения.
Чтобы реализовать оптимистичный параллелизм в EF Core, необходимо настроить свойство как токен параллелизма. Оно загружается и отслеживается вместе с сущностью. Когда вы вызываете SaveChanges, EF Core сравнивает значение токена параллелизма со значением в базе данных.
Например, в SQL Server есть столбец rowversion, который автоматически меняется при обновлении строки, поэтому это отличный вариант для токена параллелизма. Можно настроить теневое свойство в EF Core, чтобы спрятать его от основного класса сущности:
protected override void OnModelCreating(
ModelBuilder mb)
{
mb.Entity<Apart>()
.Property<byte[]>("Version")
.IsRowVersion();
}
* Точная конфигурация будет зависеть от типа вашей БД.
При загрузке сущности Apart EF также загрузит токен параллелизма.
SELECT a.Id, a.Version
FROM Aparts a
WHERE a.Id = @p0
А при вызове SaveChanges, запрос UPDATE сравнит его с текущим токеном в БД:
UPDATE Aparts a
SET a.LastBookedOnUtc = @p0
WHERE a.Id = @p1 AND a.Version = @p2;
Если версия в базе отличается, будет обновлено 0 строк, а EF Core ожидает обновления 1 строки, поэтому будет выброшено исключение DbUpdateConcurrencyException, которое надо обработать.
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/solving-race-conditions-with-ef-core-optimistic-locking