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

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

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

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

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

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

# Избежание пессимистической блокировки в Java

Пессимистическая блокировка (pessimistic locking) предполагает, что конфликты часто возникают, и данные блокируются на чтение/запись. Это может привести к deadlock-ам и снижению производительности. Вот способы её избежать.

1. Оптимистическая блокировка (Version Field)

Использование версии для контроля конфликтов:

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    private String name;
    private int balance;
    
    @Version  // Автоматическое управление версией
    private Long version;
}

@Service
public class UserService {
    private final UserRepository userRepository;
    
    public void updateBalance(Long userId, int amount) {
        User user = userRepository.findById(userId).orElseThrow();
        user.setBalance(user.getBalance() + amount);
        try {
            userRepository.save(user);  // Если версия изменилась — OptimisticLockException
        } catch (OptimisticLockException e) {
            // Повторить операцию
            updateBalance(userId, amount);
        }
    }
}

Преимущества:

  • Нет блокировок в БД
  • Лучше масштабируется
  • Обнаруживает конфликты только при сохранении

2. Использование CAS (Compare-And-Swap)

Атомарные операции для простых типов:

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private final AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Потокобезопасно
    }
    
    public int getValue() {
        return count.get();
    }
    
    public boolean compareAndSet(int expect, int update) {
        return count.compareAndSet(expect, update);
    }
}

IDEAL для счётчиков, флагов, простого состояния.

3. Read-Only транзакции

Поскольку чтение не конкурирует с другими чтениями:

@Service
public class ReportService {
    private final UserRepository userRepository;
    
    @Transactional(readOnly = true)  // Подсказка БД
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}

БД может оптимизировать запросы и не блокировать для чтения.

4. Снижение области блокировки

Миниализировать время удержания блокировки:

// ❌ Плохо — долгая блокировка
@Transactional
public void processOrder(Long orderId) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    order.setStatus("processing");
    
    // Долгая операция внутри транзакции
    callExternalService();  // 10 секунд!
    
    orderRepository.save(order);
}

// ✅ Хорошо — минимальная блокировка
public void processOrder(Long orderId) {
    // Долгая операция вне транзакции
    ExternalData data = callExternalService();  // 10 секунд
    
    // Коротка транзакция только для БД
    updateOrderInDb(orderId, data);
}

@Transactional
private void updateOrderInDb(Long orderId, ExternalData data) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    order.setStatus("processing");
    order.setData(data);
    orderRepository.save(order);
}

5. MVCC (Multi-Version Concurrency Control)

Структура MVCC позволяет читать старые версии без блокировки:

-- PostgreSQL с REPEATABLE READ изоляцией
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  SELECT * FROM users WHERE id = 1;
  -- Другая транзакция может менять эту строку
  SELECT * FROM users WHERE id = 1;  -- Видит старую версию
COMMIT;

Java сторона:

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void readWithMVCC() {
    User user1 = userRepository.findById(1L).orElseThrow();
    // В это время может быть UPDATE
    User user2 = userRepository.findById(1L).orElseThrow();
    // user1.equals(user2) == true (видит одинаковую версию)
}

6. Стратегия retry с exponential backoff

Для оптимистической блокировки:

@Service
public class PaymentService {
    private final PaymentRepository paymentRepository;
    
    public void processPayment(Long paymentId, BigDecimal amount) {
        int maxRetries = 3;
        int attempt = 0;
        
        while (attempt < maxRetries) {
            try {
                Payment payment = paymentRepository.findById(paymentId).orElseThrow();
                payment.setAmount(amount);
                payment.setStatus("completed");
                paymentRepository.save(payment);
                return;  // Успех
            } catch (OptimisticLockException e) {
                attempt++;
                if (attempt >= maxRetries) {
                    throw new PaymentProcessingException("Failed after " + maxRetries + " retries");
                }
                // Exponential backoff
                long waitMs = (long) Math.pow(2, attempt) * 100;
                Thread.sleep(waitMs);
            }
        }
    }
}

7. Избежание cyclic dependency deadlock-ов

Порядок захвата ресурсов критичен:

// ❌ Может быть deadlock
Thread 1: lock(A) -> lock(B)
Thread 2: lock(B) -> lock(A)

// ✅ Безопасно — всегда одинаковый порядок
Thread 1: lock(A) -> lock(B)
Thread 2: lock(A) -> lock(B)

В Java:

public class Transfer {
    public static void transfer(Account from, Account to, BigDecimal amount) {
        Account first = from.id < to.id ? from : to;
        Account second = from.id < to.id ? to : from;
        
        synchronized (first) {
            synchronized (second) {
                from.withdraw(amount);
                to.deposit(amount);
            }
        }
    }
}

8. Используй Read-Write Lock для чтения

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache<K, V> {
    private final Map<K, V> data = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public V get(K key) {
        lock.readLock().lock();
        try {
            return data.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    public void put(K key, V value) {
        lock.writeLock().lock();
        try {
            data.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Много читателей могут работать одновременно.

Лучшие практики

  1. Предпочитай оптимистическую блокировку по умолчанию
  2. Используй AtomicInteger/AtomicLong для простых счётчиков
  3. Минимизируй время в транзакции
  4. Используй READ_UNCOMMITTED только если уверен
  5. Тестируй race conditions с несколькими потоками
  6. Документируй стратегию блокировки для критичного кода
  7. Мониторь deadlock-и в логах и метриках
Как можно избежать пессимистическую блокировку в Java? | PrepBro