← Назад к вопросам

Когда выгоднее использовать Pessimistic lock?

2.8 Senior🔥 91 комментариев
#ORM и Hibernate#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Pessimistic Lock: когда его использовать

Pessimistic lock — это стратегия управления конкурентностью, где система захватывает блокировку на ресурс ДО его изменения, предполагая, что конфликты будут часто случаться. Это противоположность оптимистичному подходу.

Основное различие между стратегиями

Optimistic Lock (оптимистичный):
- Предполагаем: конфликты РЕДКИЕ
- Берём версию данных
- Изменяем без блокировки
- Проверяем версию при сохранении
- Если версия изменилась → откатываем и повторяем

Pessimistic Lock (пессимистичный):
- Предполагаем: конфликты ЧАСТЫЕ
- Сразу берём БЛОКИРОВКУ
- Изменяем данные
- Отпускаем блокировку
- Другие потоки ждут наше завершение

Сценарий 1: Высокая конкуренция за один ресурс

Когда много потоков пытаются одновременно обновить один и тот же объект, pessimistic lock предотвращает конфликты и пересчёты.

// ❌ Оптимистичный подход при высокой конкуренции (проблема)
public class Account {
    @Version  // версионирование
    private Long version;
    private BigDecimal balance;
}

public class TransactionService {
    @Transactional
    public void transferMoney(Long accountId, BigDecimal amount) {
        Account account = accountRepository.findById(accountId).get();
        // Между чтением и изменением прошло время
        // 10 других потоков изменили account — версия не совпадает!
        account.setBalance(account.getBalance().subtract(amount));
        accountRepository.save(account);
        // OptimisticLockException! Повторяем транзакцию...
    }
}

// При 1000 параллельных переводов:
// - 999 получат исключение
// - Система перезапустит транзакцию
// - Огромное потребление ресурсов!

// ✅ Pessimistic lock (решение)
public class TransactionService {
    @Transactional
    public void transferMoney(Long accountId, BigDecimal amount) {
        // SELECT ... FOR UPDATE — блокируем строку в БД!
        Account account = accountRepository
            .findByIdWithLock(accountId);
        
        // Другие потоки ждут, пока мы закончим
        account.setBalance(account.getBalance().subtract(amount));
        accountRepository.save(account);
        // Готово! Без переустановок транзакции
    }
}

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Optional<Account> findByIdWithLock(@Param("id") Long id);
}

Сценарий 2: Бизнес-критичные операции, где конфликты дорогие

Когда cost of retry >> cost of waiting, pessimistic lock имеет смысл.

// ❌ Оптимистичный (проблема: дорогой retry)
public class InventoryService {
    // Если запас низкий и много людей покупают одновременно,
    // при OptimisticLock множество откатится
    // Каждый retry = новая проверка запаса, расчёты доставки, etc.
    @Transactional
    public void reserveItem(Long itemId) {
        InventoryItem item = inventoryRepository.findById(itemId).get();
        if (item.getQuantity() > 0) {
            item.setQuantity(item.getQuantity() - 1);
            inventoryRepository.save(item);
        }
    }
}

// ✅ Pessimistic lock (дешевле — нет retries)
public class InventoryService {
    @Transactional
    public void reserveItem(Long itemId) {
        // Блокируем строку — другие ждут
        InventoryItem item = inventoryRepository
            .findByIdWithPessimisticLock(itemId);
        
        if (item.getQuantity() > 0) {
            item.setQuantity(item.getQuantity() - 1);
            inventoryRepository.save(item);
        }
        // Транзакция завершена успешно с первой попытки
    }
}

Сценарий 3: Операции, которые нельзя повторять

Если retry может привести к побочным эффектам, используй pessimistic lock.

// ❌ Проблема: retry может отправить два письма
public class NotificationService {
    @Transactional
    public void sendNotification(Long userId) {
        User user = userRepository.findById(userId).get();
        // OptimisticLockException между этой точкой
        sendEmail(user.getEmail());  // и этой
        user.setLastNotificationTime(now());
        userRepository.save(user);
        // При retry отправляем письмо ещё раз!
    }
}

// ✅ Pessimistic lock (гарантирует one-time execution)
public class NotificationService {
    @Transactional
    public void sendNotification(Long userId) {
        User user = userRepository
            .findByIdWithLock(userId);  // Блокируем
        
        sendEmail(user.getEmail());  // Выполнится один раз
        user.setLastNotificationTime(now());
        userRepository.save(user);
    }
}

Сценарий 4: Финансовые операции

В финансовых системах нельзя рисковать конфликтами.

@Service
public class PaymentService {
    @Transactional
    public void processPayment(PaymentRequest request) {
        // Pessimistic write lock гарантирует
        // что никто другой не будет менять баланс
        Account account = accountRepository
            .findByIdWithWriteLock(request.getAccountId());
        
        validateBalance(account, request.getAmount());
        account.debit(request.getAmount());
        accountRepository.save(account);
        
        // Никаких race conditions, никаких потерь денег!
    }
}

Виды pessimistic lock в JPA

// 1. PESSIMISTIC_READ (SELECT ... FOR SHARE)
// Несколько читателей, но писатели блокируются
@Lock(LockModeType.PESSIMISTIC_READ)
Optional<Account> findById(Long id);

// 2. PESSIMISTIC_WRITE (SELECT ... FOR UPDATE) — используй это!
// Эксклюзивная блокировка — никто другой не может читать
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Account> findById(Long id);

// 3. PESSIMISTIC_FORCE_INCREMENT
// Как PESSIMISTIC_WRITE, но ещё инкрементирует @Version
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
Optional<Account> findById(Long id);

Сценарий 5: Чтение перед изменением (Read-Modify-Write)

Когда нужно гарантированно прочитать текущее значение, используй pessimistic lock.

// ❌ Проблема: между read и write может кто-то вклиниться
Account account = accountRepository.findById(id).get();
BigDecimal newBalance = account.getBalance().add(interest);
account.setBalance(newBalance);
// Если другой поток изменил баланс между строками 1 и 3?

// ✅ Решение: pessimistic lock
Account account = accountRepository
    .findByIdWithLock(id);  // SELECT ... FOR UPDATE
BigDecimal newBalance = account.getBalance().add(interest);
account.setBalance(newBalance);
// Гарантия: никто не изменит во время операции

Когда ИЗБЕГАТЬ pessimistic lock:

  • Низкая конкуренция — используй optimistic lock
  • Много чтений, мало писаний — optimistic lock
  • Долгие транзакции — блокировка будет долгой
  • Распределённые системы — распределённые блокировки сложны

Ключевая таблица решений:

СценарийКонкуренцияRetry стоимостьВыбор
Высокая конкуренцияВысокаяДорогоPessimistic
Низкая конкуренцияНизкаяДёшевоOptimistic
Финансовые операцииПеременнаяКритичноPessimistic
Каталог товаровНизкаяДёшевоOptimistic
Бронирование местВысокаяВысокоPessimistic

Правило: Используй pessimistic lock, когда конкуренция за ресурс предсказуемо высока и cost of conflict > cost of waiting.

Когда выгоднее использовать Pessimistic lock? | PrepBro