← Назад к вопросам
Как избежать одновременную перезапись результата предыдущего изменения с помощью Hibernate?
1.8 Middle🔥 191 комментариев
#ORM и Hibernate
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как избежать перезапись изменений в Hibernate (Optimistic Locking)
Это проблема Lost Update (потеря обновления) в многопользовательских системах. Когда два пользователя одновременно редактируют один объект, второе обновление может перезаписать первое.
Проблема: Lost Update
Время Пользователь 1 Пользователь 2
─────────────────────────────────────────────
T1 Читает счёт: 1000
T2 Читает счёт: 1000
T3 Обновляет на 1100
T4 Обновляет на 900
T5 Сохраняет 1100
T6 Сохраняет 900
Результат: балнс 900 (потеряно +100 от пользователя 1!)
Решение 1: Optimistic Locking (рекомендуется)
Используем версионирование объектов через аннотацию @Version:
// Hibernate модель с версией
@Entity
@Table(name = "accounts")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountNumber;
private BigDecimal balance;
// ✅ Версия для optimistic locking
@Version
private Long version; // или @Version private Integer version;
// getters/setters
public Long getId() { return id; }
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal balance) { this.balance = balance; }
public Long getVersion() { return version; }
}
Как работает: Каждый раз при обновлении Hibernate:
- Проверяет версию в БД
- Если версия не совпадает → выбрасывает
OptimisticLockException - Разработчик обрабатывает исключение (retry, показывает ошибку, merge и т.д.)
// Service с обработкой optimistic lock exception
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferMoney(Long accountId, BigDecimal amount) {
try {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new AccountNotFoundException(accountId));
// Увеличиваем баланс
account.setBalance(account.getBalance().add(amount));
accountRepository.save(account);
// Если была конкурирующая транзакция, выбросится исключение
} catch (OptimisticLockingFailureException e) {
// Обработка конфликта
log.warn("Optimistic lock failed for account: " + accountId);
// Вариант 1: retry
// return transferMoney(accountId, amount);
// Вариант 2: выбросить custom исключение
// throw new ConcurrentUpdateException("Account updated by another user");
// Вариант 3: merge данных
// Account latest = accountRepository.findById(accountId).get();
// latest.setBalance(latest.getBalance().add(amount));
// accountRepository.save(latest);
}
}
}
Сгенерированный SQL:
-- Hibernate генерирует запрос с проверкой версии
UPDATE accounts
SET balance = ?, version = version + 1
WHERE id = ? AND version = ?;
-- Параметры:
-- balance = 1100
-- id = 1
-- version = 0 (текущая версия)
-- Если версия изменилась, обновлено 0 строк → OptimisticLockException
Решение 2: Pessimistic Locking (пессимистичная блокировка)
Блокируем запись на время редактирования:
// JPA Query с pessimistic lock
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
// Блокируем для чтения-записи
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdForUpdate(@Param("id") Long id);
// Блокируем только для чтения
@Lock(LockModeType.PESSIMISTIC_READ)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdForRead(@Param("id") Long id);
}
// Использование
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferMoney(Long accountId, BigDecimal amount) {
// Блокируем запись на время транзакции
Account account = accountRepository.findByIdForUpdate(accountId)
.orElseThrow(() -> new AccountNotFoundException(accountId));
// Пока мы обновляем, другие транзакции ждут
account.setBalance(account.getBalance().add(amount));
accountRepository.save(account);
// Блокировка снимается при коммите транзакции
}
}
Виды pessimistic lock:
| LockModeType | Описание | SQL |
|---|---|---|
| PESSIMISTIC_READ | Другие не могут читать/писать | SELECT ... FOR SHARE (или эквивалент) |
| PESSIMISTIC_WRITE | Другие не могут вообще трогать | SELECT ... FOR UPDATE |
| PESSIMISTIC_FORCE_INCREMENT | Увеличивает версию | FOR UPDATE + инкремент версии |
-- PostgreSQL
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Pessimistic write
SELECT * FROM accounts WHERE id = 1 FOR SHARE; -- Pessimistic read
-- MySQL
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
Сравнение подходов
| Аспект | Optimistic | Pessimistic |
|---|---|---|
| Когда использовать | Редкие конфликты | Частые конфликты |
| Производительность | Высокая | Ниже (ждём блокировку) |
| Deadlocks | Нет | Возможны |
| Сложность | Средняя (обработка исключения) | Низкая (БД сама блокирует) |
| Масштабируемость | Хорошая | Плохая под высокой нагрузкой |
| Примеры | Web приложения, CMS | Бухгалтерия, критичные операции |
Практический пример: Full-stack решение
// 1. Entity с версией
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
private Integer quantity;
@Version
private Long version; // Для optimistic locking
public void updatePrice(BigDecimal newPrice) {
this.price = newPrice;
}
// getters/setters
}
// 2. Repository
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {}
// 3. Service с retry логикой
@Service
@Slf4j
public class ProductService {
@Autowired
private ProductRepository repository;
private static final int MAX_RETRIES = 3;
private static final int RETRY_DELAY_MS = 100;
@Transactional
public void updateProductPrice(Long productId, BigDecimal newPrice)
throws MaxRetriesExceededException {
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
Product product = repository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
product.updatePrice(newPrice);
repository.save(product);
log.info("Product price updated successfully on attempt {}", attempt);
return; // Успех!
} catch (OptimisticLockingFailureException e) {
log.warn("Optimistic lock conflict on attempt {}/{}", attempt, MAX_RETRIES);
if (attempt == MAX_RETRIES) {
throw new MaxRetriesExceededException(
"Failed to update product after " + MAX_RETRIES + " attempts"
);
}
// Ждём перед retry
try {
Thread.sleep(RETRY_DELAY_MS * attempt);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
}
}
}
// Альтернатива: merge with conflict resolution
@Transactional
public void updateProductPriceWithMerge(Long productId, BigDecimal newPrice) {
try {
Product product = repository.findById(productId).orElseThrow();
product.updatePrice(newPrice);
repository.save(product);
} catch (OptimisticLockingFailureException e) {
// Получаем最свежую версию и меджируем
Product latest = repository.findById(productId).orElseThrow();
latest.updatePrice(newPrice);
repository.save(latest);
}
}
}
// 4. REST контроллер
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@PutMapping("/{id}/price")
public ResponseEntity<String> updatePrice(
@PathVariable Long id,
@RequestParam BigDecimal price) {
try {
productService.updateProductPrice(id, price);
return ResponseEntity.ok("Price updated");
} catch (MaxRetriesExceededException e) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body("Price update failed due to concurrent modifications. Please retry.");
}
}
}
Конфигурация в application.yml
spring:
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
enable_lazy_load_no_trans: false
# Для лучшей обработки optimistic lock exception
jpa:
show-sql: false
properties:
hibernate:
jdbc:
batch_size: 20
fetch_size: 50
Когда что использовать
Optimistic Locking:
- Web приложения, CMS
- Низкий процент одновременных редактирований
- Предпочтительно для масштабируемости
- REST API, где фронт может retry'ить
Pessimistic Locking:
- Бухгалтерия, банки
- Высокий процент конфликтов
- Критичные операции, которые не должны конфликтовать
- Короткие транзакции
Итог
Для Hibernate используй Optimistic Locking с @Version — это современный, масштабируемый подход. Добавь версионный столбец в entity, обрабатывай OptimisticLockingFailureException с retry логикой. Pessimistic Locking используй только в специальных случаях для критичных операций.