Как реализовать пессимистичную блокировку в JPA
Пессимистичная блокировка блокирует строки в базе данных на уровне СУБД, предотвращая конкурентный доступ. Другие транзакции не могут читать или писать заблокированные данные до...
Обзор
Пессимистичная блокировка блокирует строки в базе данных на уровне СУБД, предотвращая конкурентный доступ. Другие транзакции не могут читать или писать заблокированные данные до завершения текущей транзакции.
🟢 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]]