Как можно избежать пессимистическую блокировку в Java?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Избежание пессимистической блокировки в 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();
}
}
}
Много читателей могут работать одновременно.
Лучшие практики
- Предпочитай оптимистическую блокировку по умолчанию
- Используй AtomicInteger/AtomicLong для простых счётчиков
- Минимизируй время в транзакции
- Используй READ_UNCOMMITTED только если уверен
- Тестируй race conditions с несколькими потоками
- Документируй стратегию блокировки для критичного кода
- Мониторь deadlock-и в логах и метриках