Вопрос 17 · Раздел 16

Как реализовать оптимистичную блокировку в JPA

Оптимистичная блокировка — стратегия управления конкурентным доступом, которая позволяет нескольким транзакциям читать одни и те же данные, но предотвращает конфликтующие обновл...

Версии по языкам: English Russian Ukrainian

Обзор

Оптимистичная блокировка — стратегия управления конкурентным доступом, которая позволяет нескольким транзакциям читать одни и те же данные, но предотвращает конфликтующие обновления через проверку версии.


🟢 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()]]