Недавно занималась задачей, где нужно было реализовать оптимистическую блокировку сущности Hibernate при конкурентном доступе двух методов.
Задача была такая:
Сервис А обрабатывает таски. Во время обработки он делает запрос в сервис B. Бывает, что сервис B отвечает слишком медленно, и в таком случае нам хотелось бы, не дожидаясь ответа, отменять задачу и создавать другую с высоким приоритетом. Для этого я реализовала работающий по расписанию метод, чтобы он извлекал из БД и обрабатывал все задачи с истекшим временем ожидания.
Псевдокод:
// когда штатно пришел ответ от B
@Transactional
public void processTask(long id) {
TaskEntity task = taskRepository.findById(id);
if (task.getStatus() != IN_PROGRESS) {
return;
}
// some logic
task.setStatus(DONE);
taskRepository.save(task);
}
// когда время ожидания истекло
@Transactional
public void processLongAwaitingTask(long id) {
TaskEntity task = taskRepository.findById(id);
if (task.getStatus() != IN_PROGRESS) {
return;
}
task.setStatus(EXPIRED);
taskRepository.save(task);
PriorityTaskEntity priorityTask = new PriorityTaskEntity(...);
priorityTaskRepository.save(priorityTask);
}
В обоих случаях мы проверяем, что статус задачи корректный, и только потом продолжаем обработку. Но это не исключает ситуацию, когда одновременно пришел ответ от сервиса B, и истекло время ожидания задачи. И оба метода начали выполняться одновременно, и пока ни одна из транзакций не успела закоммитить изменение статуса. Теоретически можно получить вариант, когда наша задача в статусе DONE, а мы все равно создали PriorityTask, что нарушит консистентность данных. Чтобы решить проблему, можно использовать оптимистическую блокировку TaskEntity.
Для этого нужен атрибут version в таблице tasks в БД, и, соответственно, поле в сущности TaskEntity. Hibernate через это поле отслеживает изменения сущности. При сохранении он автоматически инкрементирует поле version, и выбрасывает OptimisticLockException, если поле в БД не совпадает с ожидаемым.
Поэтому, если транзакция из метода processTask закоммитит изменения первой, то мы получим исключение в методе processLongAwaitingTask, и его транзакция будет откачена. А если транзакция из processLongAwaitingTask закоммитит первой, то соответственно откатятся изменения processTask. Так сохранится консистентность данных.
Теоретически, есть другой вариант — использовать пессимистическую блокировку.
В этом случае только одна транзакция сможет получить доступ к строке из таблицы task, а другая будет ждать, пока первая не освободит данные. Пессимистические блокировки используются в условиях высокой вероятности конфликтов и сильных требований к консистентности данных, и могут замедлять процессинг и приводить к дедлокам. Но это не наш, случай, т.к. мы ожидаем, что конфликты будут редкими.