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