← Назад к вопросам
Как организовать блокировки в Spring Data
2.0 Middle🔥 201 комментариев
#Spring Boot и Spring Data#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Организация блокировок в Spring Data
Введение
Блокировки в Spring Data используются для контроля конкурентного доступа к данным. Существует два основных типа блокировок: пессимистические (pessimistic) и оптимистические (optimistic). Они защищают от race conditions и обеспечивают консистентность данных в многопоточной среде.
1. Пессимистические блокировки (Pessimistic Locking)
Блокируют данные на уровне БД, пока одна транзакция их обрабатывает.
1.1 Пессимистическая READ блокировка
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Lock;
import javax.persistence.LockModeType;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.JpaRepository;
@Entity
public class Account {
@Id
private Long id;
private BigDecimal balance;
private String accountNumber;
}
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_READ)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdWithReadLock(Long id);
}
@Service
public class AccountService {
private final AccountRepository accountRepository;
@Transactional
public BigDecimal getAccountBalance(Long accountId) {
// Получить счет с READ блокировкой
Account account = accountRepository.findByIdWithReadLock(accountId);
return account.getBalance();
}
}
1.2 Пессимистическая WRITE блокировка
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdWithWriteLock(Long id);
}
@Service
public class TransferService {
private final AccountRepository accountRepository;
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// Получить счета с WRITE блокировкой (эксклюзивная)
Account fromAccount = accountRepository.findByIdWithWriteLock(fromId);
Account toAccount = accountRepository.findByIdWithWriteLock(toId);
if (fromAccount.getBalance().compareTo(amount) >= 0) {
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
}
1.3 Force INCREMENT для версионирования
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdWithForceIncrementLock(Long id);
}
2. Оптимистические блокировки (Optimistic Locking)
Используют версионирование для обнаружения конфликтов без блокировки данных на уровне БД.
2.1 Базовое версионирование
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
@Entity
public class Product {
@Id
private Long id;
private String name;
private BigDecimal price;
@Version
private Long version; // Автоматически управляется Hibernate
public Product() {}
public Product(Long id, String name, BigDecimal price) {
this.id = id;
this.name = name;
this.price = price;
this.version = 0L;
}
}
public interface ProductRepository extends JpaRepository<Product, Long> {
}
@Service
public class ProductService {
private final ProductRepository productRepository;
@Transactional
public void updateProductPrice(Long productId, BigDecimal newPrice) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new EntityNotFoundException("Product not found"));
// При сохранении version автоматически увеличится
product.setPrice(newPrice);
productRepository.save(product);
}
}
2.2 Обработка OptimisticLockingFailureException
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.retry.annotation.Retryable;
@Service
public class OrderService {
private final OrderRepository orderRepository;
@Transactional
@Retryable(value = ObjectOptimisticLockingFailureException.class,
maxAttempts = 3, backoff = @Backoff(delay = 100))
public void updateOrder(Long orderId, String status) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
try {
order.setStatus(status);
orderRepository.save(order);
} catch (ObjectOptimisticLockingFailureException e) {
System.out.println("Conflict detected, retrying...");
throw e;
}
}
}
2.3 Пользовательское версионирование
import java.time.LocalDateTime;
@Entity
public class Document {
@Id
private Long id;
private String content;
@Version
private Long version;
@Column(name = "last_modified")
private LocalDateTime lastModified;
@PreUpdate
protected void onUpdate() {
this.lastModified = LocalDateTime.now();
}
}
3. Комбинированные подходы
3.1 Пессимистическая блокировка с версионированием
@Entity
public class InventoryItem {
@Id
private Long id;
private Integer quantity;
@Version
private Long version;
}
public interface InventoryRepository extends JpaRepository<InventoryItem, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT i FROM InventoryItem i WHERE i.id = :id")
InventoryItem findByIdForUpdate(Long id);
}
@Service
public class InventoryService {
private final InventoryRepository inventoryRepository;
@Transactional
public void decreaseQuantity(Long itemId, Integer decreaseBy)
throws OutOfStockException {
InventoryItem item = inventoryRepository.findByIdForUpdate(itemId);
if (item.getQuantity() < decreaseBy) {
throw new OutOfStockException("Not enough stock");
}
item.setQuantity(item.getQuantity() - decreaseBy);
inventoryRepository.save(item);
}
}
4. SELECT FOR UPDATE (Raw SQL)
public interface AccountRepository extends JpaRepository<Account, Long> {
@Query(value = "SELECT * FROM accounts WHERE id = :id FOR UPDATE",
nativeQuery = true)
Account lockForUpdate(Long id);
}
5. Практический пример: Система заказов
@Entity
public class Order {
@Id
private Long id;
private BigDecimal totalAmount;
private OrderStatus status;
@Version
private Long version;
}
@Entity
public class Payment {
@Id
private Long id;
@ManyToOne
private Order order;
private BigDecimal amount;
private PaymentStatus status;
}
public interface OrderRepository extends JpaRepository<Order, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT o FROM Order o WHERE o.id = :id")
Order findByIdForPayment(Long id);
}
@Service
public class PaymentProcessingService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
@Transactional
public void processPayment(Long orderId, BigDecimal amount)
throws InvalidOrderException, PaymentFailedException {
// Получить заказ с WRITE блокировкой
Order order = orderRepository.findByIdForPayment(orderId);
if (!order.getStatus().equals(OrderStatus.PENDING)) {
throw new InvalidOrderException("Order is not pending");
}
try {
// Выполнить платеж
Payment payment = new Payment();
payment.setOrder(order);
payment.setAmount(amount);
payment.setStatus(PaymentStatus.PROCESSING);
paymentRepository.save(payment);
// Обновить статус заказа
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
payment.setStatus(PaymentStatus.COMPLETED);
paymentRepository.save(payment);
} catch (Exception e) {
throw new PaymentFailedException("Payment processing failed", e);
}
}
}
6. Таблица сравнения
| Тип | Блокировка БД | Обнаружение конфликтов | Производительность | Когда использовать |
|---|---|---|---|---|
| PESSIMISTIC_READ | Да | Автоматически | Низкая | Частые конфликты |
| PESSIMISTIC_WRITE | Да | Автоматически | Низкая | Критичные данные |
| OPTIMISTIC | Нет | При сохранении | Высокая | Редкие конфликты |
| Version | Нет | По версии | Высокая | Обычный случай |
7. Лучшие практики
@Service
public class BestPracticesService {
// ✓ Правильно: используй @Transactional с явной стратегией
@Transactional
@Retryable(value = ObjectOptimisticLockingFailureException.class)
public void handleConcurrentUpdate(Long id) {
// логика
}
// ✓ Правильно: явное обозначение блокировки
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT e FROM Entity e WHERE e.id = :id")
Entity getForUpdate(Long id);
// ✗ Неправильно: без явной блокировки при критичных данных
Entity getSimple(Long id);
}
Заключение
Выбор между пессимистической и оптимистической блокировкой зависит от:
- Пессимистическая: высокая частота конфликтов, критичные данные
- Оптимистическая: низкая частота конфликтов, лучшая масштабируемость
Для большинства приложений оптимистическое версионирование (с @Version) обеспечивает хороший баланс между производительностью и безопасностью.