Как реализовать оптимистичную блокировку в JPA
Оптимистичная блокировка — стратегия управления конкурентным доступом, которая позволяет нескольким транзакциям читать одни и те же данные, но предотвращает конфликтующие обновл...
Обзор
Оптимистичная блокировка — стратегия управления конкурентным доступом, которая позволяет нескольким транзакциям читать одни и те же данные, но предотвращает конфликтующие обновления через проверку версии.
🟢 Junior Level
Что такое оптимистичная блокировка
Оптимистичная блокировка позволяет нескольким транзакциям читать одни и те же данные, но при обновлении проверяет, что данные не были изменены другой транзакцией.
Реализуется через аннотацию @Version:
@Entity
public class Order {
@Id
private Long id;
@Version
private Integer version; // Hibernate автоматически увеличивает
private String status;
}
Как работает
// Транзакция 1
Order order1 = em.find(Order.class, 1L); // version = 1
// Транзакция 2
Order order2 = em.find(Order.class, 1L); // version = 1
order2.setStatus("shipped");
em.flush(); // version = 2, UPDATE WHERE version = 1
// Транзакция 1
order1.setStatus("cancelled");
em.flush(); // ❌ OptimisticLockException!
// UPDATE WHERE version = 1 → 0 rows updated → исключение
Почему “оптимистичная”
Потому что мы оптимистично предполагаем, что конфликтов не будет, и не блокируем данные на чтение. Конфликт обнаруживается только при записи.
🟡 Middle Level
LockModeType для оптимистичной блокировки
// OPTIMISTIC — проверка version при commit/flush
em.lock(order, LockModeType.OPTIMISTIC);
// OPTIMISTIC_FORCE_INCREMENT — всегда увеличивает version
em.lock(order, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
// Даже если order не изменялся, version увеличится
Обработка OptimisticLockException
OptimisticLockException содержит информацию о сущности и ожидаемой/фактической версии. В логах: “Row was updated or deleted by another transaction”.
try {
order.setStatus("cancelled");
entityManager.flush();
} catch (OptimisticLockException e) {
// Обновить данные из БД и повторить
entityManager.refresh(order);
// retry logic
order.setStatus("cancelled");
entityManager.flush();
}
Retry с Spring
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
@Transactional
public Order updateOrder(OrderDto dto) {
Order order = entityManager.find(Order.class, dto.id());
order.setStatus(dto.status());
return order;
}
Типичные ошибки
// ❌ Нет @Version — нет оптимистичной блокировки
@Entity
public class Order {
// нет @Version → конфликты не обнаружатся
}
// ❌ Ручное изменение version
order.setVersion(0); // ❌ сломает механизм
// ❌ Игнорирование OptimisticLockException
try {
em.flush();
} catch (OptimisticLockException e) {
// молча проигнорировали → данные потеряны
}
🔴 Senior Level
Внутренняя реализация
@Version → version column в БД
При UPDATE:
UPDATE orders SET status = ?, version = version + 1
WHERE id = ? AND version = ?
Если 0 rows updated → OptimisticLockException
Процесс:
1. При INSERT: version = 0 (для Integer/Long). При первом UPDATE: version = 1.
2. При UPDATE: version = version + 1
3. WHERE clause включает version check
4. Если version не совпал → 0 rows → исключение
Типы полей version
@Version
private Integer version; // int — автоматически увеличивается
@Version
private Long version; // long — автоматически увеличивается
@Version
private Timestamp version; // timestamp — обновляется автоматически
Когда использовать OPTIMISTIC_FORCE_INCREMENT
// Когда нужно увеличить version без изменения entity
// Например, при изменении дочерней коллекции
@Entity
public class Order {
@Version
private Integer version;
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
// При изменении items — нужно увеличить version Order
em.lock(order, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
// version Order увеличится, хотя сам Order не менялся
Production паттерны
// Pattern 1: Retry с exponential backoff
@Retryable(
value = OptimisticLockException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2)
)
@Transactional
public Order updateWithRetry(OrderDto dto) {
Order order = entityManager.find(Order.class, dto.id());
order.setStatus(dto.status());
return order;
}
// Pattern 2: Refresh and retry вручную
@Transactional
public Order updateWithManualRetry(OrderDto dto) {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
Order order = entityManager.find(Order.class, dto.id());
order.setStatus(dto.status());
entityManager.flush();
return order;
} catch (OptimisticLockException e) {
if (i == maxRetries - 1) throw e;
entityManager.clear(); // очистить и попробовать снова
}
}
throw new IllegalStateException("Unreachable");
}
Оптимистичная vs пессимистичная
| Оптимистичная | Пессимистичная | |
|---|---|---|
| Блокировка | Нет (только при записи) | Да (при чтении) |
| Конфликт | Обнаруживается при записи | Предотвращается при чтении |
| Производительность | Высокая (нет блокировок) | Ниже (блокировки) |
| Deadlock | Невозможен | Возможен |
| Когда | Редкие конфликты | Частые конфликты |
Best Practices
✅ @Version на всех mutable сущностях
✅ Retry при OptimisticLockException
✅ OPTIMISTIC_FORCE_INCREMENT когда нужно
✅ @Retryable для автоматического retry
✅ Мониторинг частоты конфликтов
❌ Без @Version для важных данных
❌ Без retry логики
❌ Ручное изменение version
❌ Пессимистичная блокировка без причины
❌ Игнорирование OptimisticLockException
🎯 Шпаргалка для интервью
Обязательно знать:
- Оптимистичная блокировка через @Version — автоматически увеличивает version при каждом UPDATE
- WHERE clause включает version check: WHERE id = ? AND version = ?
- При конфликте (0 rows updated) → OptimisticLockException
- LockModeType: OPTIMISTIC (проверка version), OPTIMISTIC_FORCE_INCREMENT (всегда увеличивает)
- Retry паттерн: @Retryable(value = OptimisticLockException.class, maxAttempts = 3)
- Типы version: Integer, Long (увеличиваются автоматически), Timestamp (опасен — конфликт в одну миллисекунду)
Частые уточняющие вопросы:
- Почему “оптимистичная”? Предполагаем что конфликтов не будет, блокируем только при записи
- Когда OPTIMISTIC_FORCE_INCREMENT? При изменении дочерней коллекции, для инвалидации кэша
- Что лучше — optimistic vs pessimistic? Optimistic — высокая производительность, редкие конфликты; Pessimistic — частые конфликты, критичные данные
- Timestamp vs Integer для @Version? Integer надёжнее — Timestamp может пропустить конфликт при обновлении в одну миллисекунду
Красные флаги (НЕ говорить):
- «Без @Version для финансовых данных» — lost updates возможны
- «Ручное изменение version» — сломает механизм блокировки
- «Игнорирую OptimisticLockException» — данные потеряны
- «Пессимистичная блокировка по умолчанию» — ненужные блокировки, deadlock возможны
Связанные темы:
- [[18. Как реализовать пессимистичную блокировку в JPA]]
- [[19. Что такое @Version и зачем она нужна]]
- [[13. Как работает механизм flush в Hibernate]]
- [[15. Что делает метод refresh()]]