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

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

Пессимистичная блокировка блокирует строки в базе данных на уровне СУБД, предотвращая конкурентный доступ. Другие транзакции не могут читать или писать заблокированные данные до...

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

Обзор

Пессимистичная блокировка блокирует строки в базе данных на уровне СУБД, предотвращая конкурентный доступ. Другие транзакции не могут читать или писать заблокированные данные до завершения текущей транзакции.


🟢 Junior Level

Что такое пессимистичная блокировка

Пессимистичная блокировка — блокирует строку в БД на уровне СУБД. Другие транзакции не могут читать/писать заблокированные данные.

// Блокировка на запись — другие не могут читать/писать
Order order = entityManager.find(Order.class, 1L,
    LockModeType.PESSIMISTIC_WRITE);

// Блокировка на чтение — другие могут читать, но не писать
Order order = entityManager.find(Order.class, 1L,
    LockModeType.PESSIMISTIC_READ);

// NOWAIT — не ждать, сразу ошибка если заблокировано
Order order = entityManager.find(Order.class, 1L,
    LockModeType.PESSIMISTIC_WRITE,
    Map.of("jakarta.persistence.lock.timeout", 0));

Когда использовать

  • Критические данные, которые часто обновляются
  • Когда конфликты частые (оптимистичная блокировка не подходит)
  • Финансовые операции, инвентарь, бронирование

🟡 Middle Level

LockModeType

PESSIMISTIC_READ:
- SELECT ... FOR SHARE (PostgreSQL) (PostgreSQL 9.3+; в более старых версиях использовался FOR UPDATE)
- Другие транзакции могут читать, но не могут писать

PESSIMISTIC_WRITE:
- SELECT ... FOR UPDATE
- Другие транзакции не могут читать или писать

PESSIMISTIC_FORCE_INCREMENT:
- UPDATE + увеличение version
- Комбинация пессимистичной блокировки и optimistic versioning

Timeout

// Таймаут 5 секунд — ждать максимум 5 секунд
entityManager.find(Order.class, id,
    LockModeType.PESSIMISTIC_WRITE,
    Map.of("jakarta.persistence.lock.timeout", 5000));

// NOWAIT — не ждать вообще
entityManager.find(Order.class, id,
    LockModeType.PESSIMISTIC_WRITE,
    Map.of("jakarta.persistence.lock.timeout", 0));

Пример использования

@Transactional
public Order processOrder(Long id) {
    // Блокировка на запись — другие транзакции ждут
    Order order = entityManager.find(Order.class, id,
        LockModeType.PESSIMISTIC_WRITE);

    // Обработка — другие транзакции ждут завершения
    order.setStatus("processing");
    order.setProcessedAt(LocalDateTime.now());

    return order;
    // При commit — блокировка снимается
}

Deadlock

Транзакция 1: блокирует A → ждёт B
Транзакция 2: блокирует B → ждёт A
Deadlock! → одна транзакция будет откатана БД

// Обработка deadlock
try {
    Order order = entityManager.find(Order.class, id,
        LockModeType.PESSIMISTIC_WRITE);
} catch (PessimisticLockException | LockAcquisitionException e) {
    // retry или обработка ошибки
}

Типичные ошибки

// ❌ Долгая транзакция с блокировкой
@Transactional
public void longProcess(Long id) {
    Order order = entityManager.find(Order.class, id,
        LockModeType.PESSIMISTIC_WRITE);
    // ... долгая обработка ...
    // Другие транзакции ждут всё это время!
}

// ✅ Короткая транзакция
@Transactional
public Order lockAndProcess(Long id) {
    Order order = entityManager.find(Order.class, id,
        LockModeType.PESSIMISTIC_WRITE);
    order.setStatus("processing");
    return order;  // быстро завершить
}

🔴 Senior Level

Внутренняя реализация

PostgreSQL: SELECT ... FOR UPDATE
MySQL:      SELECT ... FOR UPDATE
Oracle:     SELECT ... FOR UPDATE NOWAIT / WAIT n
SQL Server: SELECT ... WITH (UPDLOCK, ROWLOCK)

Hibernate генерирует правильный SQL для каждой СУБД.

NOWAIT и SKIP LOCKED

// NOWAIT — сразу ошибка если заблокировано
Map<String, Object> nowait = Map.of(
    "jakarta.persistence.lock.timeout", 0
);

// SKIP LOCKED — пропустить заблокированные строки (Hibernate 6+)
// Полезно для очередей задач
//
// SKIP LOCKED — пропустить заблокированные строки. В JPA это реализуется через
// query hint 'jakarta.persistence.lock.scope' = EXTENDED, но полная поддержка
// зависит от провайдера и версии Hibernate. В Hibernate 6+ это работает стабильно.
@Query("SELECT t FROM Task t WHERE t.status = 'PENDING'")
List<Task> findAvailableTasks(
    @Param("jakarta.persistence.lock.scope") LockScope EXTENDED,
    @Param("jakarta.persistence.lock.timeout", 0)
);

Блокировка на уровне запроса

// Блокировка в JPQL запросе
@Query("SELECT o FROM Order o WHERE o.status = :status")
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Order> findOrdersForUpdate(@Param("status") String status);

// Блокировка в native query
@Query(value = "SELECT * FROM orders WHERE id = :id FOR UPDATE",
       nativeQuery = true)
Order findByIdForUpdate(@Param("id") Long id);

Production паттерны

// Pattern 1: Бронирование с блокировкой
@Transactional
public boolean reserveItem(Long itemId, int quantity) {
    Item item = entityManager.find(Item.class, itemId,
        LockModeType.PESSIMISTIC_WRITE);

    if (item.getStock() >= quantity) {
        item.setStock(item.getStock() - quantity);
        return true;
    }
    return false;
}

// Pattern 2: Queue processing с SKIP LOCKED
@QueryHints({
    @QueryHint(name = "jakarta.persistence.lock.timeout", value = "0"),
    @QueryHint(name = "jakarta.persistence.lock.scope", value = "EXTENDED")
})
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT t FROM Task t WHERE t.status = 'PENDING' ORDER BY t.createdAt")
List<Task> findNextAvailableTask();

Оптимизация

✅ Короткие транзакции с блокировкой
✅ Timeout для предотвращения долгого ожидания
✅ Индексы для locked columns
✅ Retry при deadlock
✅ SKIP LOCKED для очередей

❌ Долгие транзакции с блокировкой
❌ Без timeout
❌ Без индексации locked columns
❌ Блокировка без необходимости
❌ Игнорирование PessimisticLockException

Мониторинг

// Мониторинг блокировок в PostgreSQL
@Query(value = """
    SELECT blocked_locks.pid     AS blocked_pid,
           blocking_locks.pid    AS blocking_pid,
           blocked_activity.query AS blocked_statement
    FROM pg_catalog.pg_locks blocked_locks
    JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
    JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
    WHERE NOT blocked_locks.granted
    """, nativeQuery = true)
List<Object[]> findBlockedQueries();

Best Practices

✅ Короткие транзакции
✅ Timeout для блокировки
✅ Индексы для locked columns
✅ Retry при deadlock
✅ SKIP LOCKED для очередей
✅ PESSIMISTIC_WRITE для критических данных

❌ Долгие транзакции с блокировкой
❌ Без timeout
❌ Без индексации
❌ Блокировка для read-only операций
❌ Игнорирование LockAcquisitionException

🎯 Шпаргалка для интервью

Обязательно знать:

  • Пессимистичная блокировка — SELECT … FOR UPDATE на уровне СУБД
  • LockModeType: PESSIMISTIC_READ (FOR SHARE), PESSIMISTIC_WRITE (FOR UPDATE)
  • Timeout: jakarta.persistence.lock.timeout (0 = NOWAIT, иначе миллисекунды)
  • Deadlock возможен — одна транзакция будет откатана БД
  • SKIP LOCKED (Hibernate 6+) — пропустить заблокированные строки, полезно для очередей
  • Короткие транзакции — другие транзакции ждут завершения блокировки

Частые уточняющие вопросы:

  • Когда pessimistic лучше optimistic? Частые конфликты, критические данные, финансовые операции
  • Что такое SKIP LOCKED? Пропускает заблокированные строки — идеально для очередей задач
  • READ vs WRITE блокировка? READ — другие могут читать но не писать; WRITE — другие не могут читать/писать
  • Как обработать deadlock? Retry logic, мониторинг blocked queries, индексы для locked columns

Красные флаги (НЕ говорить):

  • «Долгая транзакция с блокировкой» — другие транзакции ждут всё время
  • «Без timeout для блокировки» — бесконечное ожидание
  • «Пессимистичная для read-only» — ненужная блокировка
  • «Игнорирую LockAcquisitionException» — deadlock не обработан

Связанные темы:

  • [[17. Как реализовать оптимистичную блокировку в JPA]]
  • [[19. Что такое @Version и зачем она нужна]]
  • [[15. Что делает метод refresh()]]
  • [[13. Как работает механизм flush в Hibernate]]