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

Как сделать оптимистичную блокировку в Hibernate

2.0 Middle🔥 131 комментариев
#Другое

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

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

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

Оптимистичная блокировка в Hibernate

Оптимистичная блокировка — это механизм управления конкурентным доступом, который предполагает, что конфликты редкие, и проверяет версию объекта перед сохранением.

Основная идея

Вместо блокировки строки в БД (пессимистичная блокировка), оптимистичная блокировка использует версионирование. Перед обновлением проверяется версия: если она изменилась, значит другой процесс уже обновил запись.

Сравнение подходов

Пессимистичная блокировка:

  • Блокирует строку в БД при SELECT FOR UPDATE
  • Гарантирует, что только один процесс может изменить запись
  • Может привести к deadlocks
  • Лучше для частых конфликтов

Оптимистичная блокировка:

  • Не блокирует строку
  • При конфликте выбрасывает OptimisticLockException
  • Лучше для редких конфликтов
  • Улучшает пропускную способность

Реализация с @Version

1. Добавить колонку версии в таблицу

CREATE TABLE products (
    id UUID PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10, 2),
    quantity INT,
    version INT DEFAULT 0,  -- Колонка версии
    updated_at TIMESTAMP
);

2. Добавить @Version аннотацию в entity

@Entity
@Table(name = "products")
public class Product {
    @Id
    private UUID id;
    
    private String name;
    private BigDecimal price;
    private Integer quantity;
    
    @Version
    private Long version;  // Hibernate автоматически управляет этой колонкой
    
    private LocalDateTime updatedAt;
    
    // Конструкторы и методы
    public Product() {}
    
    public Product(String name, BigDecimal price, Integer quantity) {
        this.name = name;
        this.price = price;
        this.quantity = quantity;
    }
    
    public void updateQuantity(int newQuantity) {
        this.quantity = newQuantity;
        this.updatedAt = LocalDateTime.now();
    }
}

Как это работает

1. Первоначальное чтение

Product product = productRepository.findById(id).get();
// Из БД: product.version = 1

2. Изменение объекта

product.updateQuantity(50);
// version все ещё = 1 в памяти

3. Сохранение

productRepository.save(product);
// Hibernate генерирует:
// UPDATE products SET quantity = 50, version = 2
// WHERE id = ? AND version = 1;

// Если version != 1 (изменилась), выбрасывается OptimisticLockException

Обработка OptimisticLockException

@Service
public class ProductService {
    private final productRepository;
    private final logger;
    private static final int MAX_RETRIES = 3;
    
    @Transactional
    public void updateProduct(UUID productId, int newQuantity) {
        for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
            try {
                Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new ProductNotFoundException(productId));
                
                product.setQuantity(newQuantity);
                productRepository.save(product);
                
                logger.info("Product updated successfully");
                return;
                
            } catch (OptimisticLockException e) {
                logger.warn("Optimistic lock failed, attempt " + (attempt + 1));
                
                if (attempt == MAX_RETRIES - 1) {
                    throw new ConcurrentUpdateException(
                        "Failed to update product after " + MAX_RETRIES + " attempts",
                        e
                    );
                }
                // Retry
            }
        }
    }
}

Практические примеры

Пример 1: Обновление счета

@Entity
public class BankAccount {
    @Id
    private UUID id;
    
    private String accountNumber;
    private BigDecimal balance;
    
    @Version
    private Long version;
    
    // Метод для перевода денег
    public void transfer(BigDecimal amount) throws InsufficientFundsException {
        if (balance.compareTo(amount) < 0) {
            throw new InsufficientFundsException("Insufficient balance");
        }
        this.balance = balance.subtract(amount);
    }
}

@Service
public class TransferService {
    private final accountRepository;
    
    @Transactional
    public void transferMoney(UUID fromId, UUID toId, BigDecimal amount) {
        BankAccount from = accountRepository.findById(fromId).get();
        BankAccount to = accountRepository.findById(toId).get();
        
        try {
            from.transfer(amount);
            to.receive(amount);
            
            accountRepository.save(from);  // Может выбросить OptimisticLockException
            accountRepository.save(to);    // Может выбросить OptimisticLockException
            
        } catch (OptimisticLockException e) {
            // Переполнение произошло, нужно повторить попытку
            throw new ConcurrentTransferException("Transfer conflict", e);
        }
    }
}

Пример 2: Оптимистичная блокировка с явной проверкой версии

@Service
public class VersionedUpdateService {
    private final productRepository;
    
    @Transactional
    public Product updateProductWithVersionCheck(UUID id, long expectedVersion, 
                                                 String newName) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
        
        // Проверить версию перед обновлением
        if (!product.getVersion().equals(expectedVersion)) {
            throw new ConcurrentUpdateException(
                "Product was modified by another user. " +
                "Expected version: " + expectedVersion + ", " +
                "Actual version: " + product.getVersion()
            );
        }
        
        product.setName(newName);
        return productRepository.save(product);
    }
}

Другие типы версионирования

Integer версия

@Entity
public class Product {
    @Version
    private Integer version;  // Простое целое число
}

Timestamp версия

@Entity
public class Product {
    @Version
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModified;  // Использует timestamp вместо счетчика
}

REST API с оптимистичной блокировкой

@RestController
@RequestMapping("/api/products")
public class ProductController {
    private final productService;
    
    @PutMapping("/{id}")
    public ResponseEntity<ProductDTO> updateProduct(
        @PathVariable UUID id,
        @RequestBody UpdateProductRequest request,
        @RequestHeader("If-Match") Long version) {
        
        try {
            Product updated = productService.updateProductWithVersion(
                id, 
                version,
                request.getName()
            );
            return ResponseEntity.ok(ProductDTO.from(updated));
            
        } catch (ConcurrentUpdateException e) {
            // 409 Conflict — стандартный HTTP статус для конфликтов
            return ResponseEntity.status(HttpStatus.CONFLICT)
                .build();
        }
    }
}

Конфигурация Hibernate

# application.yml
spring:
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        # Включить логирование версионирования
        enable_lazy_load_no_trans: true
        jdbc:
          batch_size: 20

Сравнение с пессимистичной блокировкой

// Пессимистичная блокировка
@Query("SELECT p FROM Product p WHERE p.id = ?1")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Product findByIdForUpdate(UUID id);

@Transactional
public void updateWithPessimisticLock(UUID id) {
    Product p = repo.findByIdForUpdate(id);  // Блокирует строку
    p.setQuantity(50);
    repo.save(p);
}  // Освобождает блокировку при коммите

// Оптимистичная блокировка
@Transactional
public void updateWithOptimisticLock(UUID id) {
    Product p = repo.findById(id).get();  // Не блокирует
    p.setQuantity(50);
    repo.save(p);  // Выбросит исключение если версия изменилась
}

Когда использовать

Используй оптимистичную блокировку когда:

  • Конфликты редкие
  • Нужна высокая пропускная способность
  • Длительные транзакции
  • Распределенные системы

Используй пессимистичную блокировку когда:

  • Конфликты частые
  • Критична консистентность
  • Короткие транзакции
  • Необходимо гарантировать exclusive access

Резюме

Оптимистичная блокировка в Hibernate:

  • Использует @Version аннотацию
  • Не блокирует строки в БД
  • При конфликте выбрасывает OptimisticLockException
  • Требует обработки исключений и retry логики
  • Лучше для систем с низким уровнем конфликтов
  • Автоматически управляется Hibernate при каждом обновлении
Как сделать оптимистичную блокировку в Hibernate | PrepBro