Как реализовать оптимистическую блокировку (Optimistic Locking) в Spring Data JPA?

«Как реализовать оптимистическую блокировку (Optimistic Locking) в Spring Data JPA?» — вопрос из категории Spring, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Оптимистическая блокировка в Spring Data JPA реализуется через механизм версионирования сущностей. Она предотвращает "потерю обновлений" (lost update) в многопользовательских сценариях без физических блокировок в БД.

1. Базовая реализация: Добавьте поле с аннотацией @Version в сущность.

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal balance;

    @Version // Ключевая аннотация
    private Long version; // Тип может быть Long, Integer, Timestamp

    // Геттеры и сеттеры
}

2. Как это работает:

  1. При загрузке сущности JPA фиксирует значение version.
  2. При обновлении в UPDATE-запрос добавляется условие WHERE id=? AND version=?.
  3. Если версия в БД изменилась (обновил другой поток/транзакция), количество обновлённых строк будет 0.
  4. JPA выбрасывает OptimisticLockException (обёрнутое в JpaSystemException или ObjectOptimisticLockingFailureException в Spring).

3. Пример использования и обработки конфликта:

@Service
public class AccountService {
    @Transactional
    public void transferMoney(Long accountId, BigDecimal amount) {
        try {
            Account account = accountRepository.findById(accountId).orElseThrow();
            account.setBalance(account.getBalance().add(amount));
            // При вызове save() выполнится проверка версии
            accountRepository.save(account);
        } catch (ObjectOptimisticLockingFailureException ex) {
            // Стратегия обработки: повторить операцию, уведомить пользователя
            throw new RetryableException("Конфликт версий. Попробуйте ещё раз.", ex);
        }
    }
}

Ключевые моменты:

  • Тип поля: Должен быть Long, Integer, java.sql.Timestamp или java.time.Instant.
  • Изменение: Поле обновляется автоматически JPA, не меняйте его вручную.
  • Когда использовать: Идеально для read-heavy сценариев с редкими конфликтующими обновлениями.
  • Альтернатива: Для write-heavy сценариев с частыми конфликтами рассмотрите пессимистическую блокировку (@Lock(LockModeType.PESSIMISTIC_WRITE)).