Что нужно учитывать при использовании оптимистичных блокировок?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Оптимистичные блокировки (Optimistic Locking)
Оптимистичные блокировки - это механизм управления конкурентным доступом к данным. Это важный паттерн в enterprise приложениях, где нужно избежать потери данных при одновременных изменениях. Рассмотрю ключевые аспекты, которые нужно учитывать.
Что такое оптимистичная блокировка
Оптимистичная блокировка предполагает, что конфликты редки, поэтому она не блокирует доступ к данным. Вместо этого она проверяет, изменились ли данные перед сохранением.
// Пессимистичная блокировка (SELECT FOR UPDATE)
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdForUpdate(@Param("id") Long id, LockModeType lockMode);
}
// Оптимистичная блокировка (версионирование)
@Entity
@Table(name = "accounts")
public class Account {
@Id
private Long id;
private BigDecimal balance;
@Version // Это поле для оптимистичной блокировки
private Long version;
}
1. Версионирование данных
Основный механизм - добавление версии к сущности.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@Version // Hibernate автоматически управляет этой версией
private Long version; // или Integer
// getters and setters
}
// Когда происходит UPDATE, Hibernate добавляет WHERE версия
// UPDATE users SET name = ?, version = ? WHERE id = ? AND version = ?
2. OptimisticLockException при конфликте
Если версия не совпадает, происходит исключение.
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public User updateUser(Long id, String newName) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
user.setName(newName); // Изменяем данные
try {
return userRepository.save(user); // Может выбросить OptimisticLockException
} catch (OptimisticLockingFailureException ex) {
// Другой процесс уже изменил этого пользователя
// Нужно перезагрузить и повторить
throw new ConcurrentModificationException("User was modified by another process");
}
}
}
3. Retry логика для обработки конфликтов
Важный момент: нужно правильно обработать исключение.
@Service
public class UserService {
private final UserRepository userRepository;
private static final int MAX_RETRIES = 3;
@Transactional
public User updateUserWithRetry(Long id, String newName) {
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
user.setName(newName);
return userRepository.save(user);
} catch (OptimisticLockingFailureException ex) {
if (attempt == MAX_RETRIES) {
throw ex; // После максимума попыток - выбросить
}
// Перезагрузить и повторить
Thread.sleep(100 * attempt); // Backoff
}
}
return null; // Никогда не дойдем сюда
}
}
// Альтернативно через аннотацию @Retryable (Spring Retry)
@Service
public class OptimisticLockService {
@Retryable(
retryFor = OptimisticLockingFailureException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2)
)
@Transactional
public User updateUser(Long id, String newName) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
user.setName(newName);
return userRepository.save(user);
}
}
4. Выбор между пессимистичной и оптимистичной блокировкой
Оптимистичная блокировка когда:
- Конфликты редки
- Низкая нагрузка на запись
- Допустимо перечитать данные и повторить операцию
// Пример: обновление профиля пользователя
@Entity
public class UserProfile {
@Id
private Long id;
private String bio;
private String profilePictureUrl;
@Version
private Long version; // Оптимистичная блокировка
}
Пессимистичная блокировка когда:
- Конфликты часто
- Высокая нагрузка на запись
- Нельзя перечитать и повторить
// Пример: перевод денег между счетами
@Service
public class PaymentService {
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// Используем SELECT FOR UPDATE (пессимистичная блокировка)
Account from = accountRepository.findByIdWithLock(fromId);
Account to = accountRepository.findByIdWithLock(toId);
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
from.withdraw(amount);
to.deposit(amount);
}
}
// Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdWithLock(@Param("id") Long id);
}
5. Проблема с detached entities
Оптимистичная блокировка работает только с managed entities.
@Service
public class UserService {
@Transactional
public void updateUser(User user) {
// ПЛОХО: user может быть detached
user.setName("New Name");
// При сохранении Hibernate не знает старую версию
userRepository.save(user);
}
@Transactional
public void updateUserCorrectly(Long id, String newName) {
// ХОРОШО: сначала загружаем, потом изменяем
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
user.setName(newName); // Entity managed, версия известна
userRepository.save(user);
}
}
6. Версия может быть разных типов
// Числовая версия (рекомендуется)
@Entity
public class User {
@Id
private Long id;
@Version
private Long version; // или Integer
}
// Временная версия (timestamp)
@Entity
public class Article {
@Id
private Long id;
@Version
@Temporal(TemporalType.TIMESTAMP)
private Date lastModified; // Hibernate автоматически обновляет
}
7. REST API и оптимистичная блокировка
Для REST API нужно передавать версию клиенту.
// DTO с версией
@Data
public class UserDto {
private Long id;
private String name;
private String email;
private Long version; // Отправляем клиенту
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(mapToDto(user)); // Версия в DTO
}
@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@RequestBody UpdateUserRequest request) { // request содержит версию
User updated = userService.updateUser(id, request);
return ResponseEntity.ok(mapToDto(updated));
}
}
@Data
public class UpdateUserRequest {
private String name;
private String email;
private Long version; // Клиент отправляет версию
}
8. Обработка конфликта на фронте
@Service
public class UserService {
@Transactional
public void updateUser(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
// Проверяем, что версия совпадает
if (!user.getVersion().equals(request.getVersion())) {
throw new OptimisticLockException(
"User has been modified by another user. Refresh and try again."
);
}
user.setName(request.getName());
user.setEmail(request.getEmail());
userRepository.save(user);
}
}
// В REST контроллере
@ExceptionHandler(OptimisticLockException.class)
public ResponseEntity<ErrorResponse> handleOptimisticLock(OptimisticLockException ex) {
ErrorResponse error = new ErrorResponse(
"CONFLICT_VERSION",
"Data was modified by another user. Please refresh and try again.",
409 // Conflict status
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
Резюме: ключевые моменты оптимистичной блокировки
- @Version аннотация - добавляем версию к entity
- OptimisticLockingFailureException - обрабатываем конфликты
- Retry логика - повторяем операцию при конфликте
- Managed entities - работаем только с entity, загруженными из БД
- Выбор механизма - оптимистичная для редких конфликтов, пессимистичная для частых
- REST API версионирование - передаем версию в DTO
- Backoff стратегия - экспоненциальный backoff при retry'ях
- Обработка ошибок - правильно обрабатываем исключения на всех уровнях
Правильное использование оптимистичной блокировки защищает от потери данных в конкурентной среде.