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

Как организовать блокировки в 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) обеспечивает хороший баланс между производительностью и безопасностью.

Как организовать блокировки в Spring Data | PrepBro