Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ: Аннотация @Version в JPA и оптимистичная блокировка
@Version — это аннотация в JPA (Java Persistence API), которая реализует механизм оптимистичной блокировки (optimistic locking) для предотвращения конфликтов при одновременном обновлении одного и того же объекта несколькими потоками или транзакциями.
Проблема: конфликты при одновременном обновлении
Представьте ситуацию:
// Поток 1 и Поток 2 одновременно обновляют один User
User user = userRepository.findById(1);
// user имеет баланс = 1000
// Поток 1 Поток 2
user.setBalance(900); user.setBalance(950);
userRepository.save(user); userRepository.save(user);
// Сохранён: баланс 900 Сохранён: баланс 950
// Результат: потеря данных!
Оба потока прочитали одно значение (1000), изменили его по-своему, и второе обновление перезаписало первое. Это называется lost update (потеря обновления).
Решение: @Version
import javax.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
private String name;
private BigDecimal balance;
// Версия для оптимистичной блокировки
@Version
private Long version; // или Integer, или Long
// getters и setters
}
Как это работает
Шаг 1: Чтение
Поток 1: SELECT id, name, balance, version FROM users WHERE id = 1
Результат: version = 1
Шаг 2: Обновление
Поток 1: UPDATE users
SET balance = 900, version = 2
WHERE id = 1 AND version = 1
Результат: успешно (1 строка обновлена)
Шаг 3: Если другой поток пытался обновить
Поток 2: UPDATE users
SET balance = 950, version = 2
WHERE id = 1 AND version = 1
Результат: ОШИБКА! (0 строк обновлено - версия уже 2)
Исключение: OptimisticLockException
Пример кода
// Entity
@Entity
@Table(name = "bank_accounts")
public class BankAccount {
@Id
private Long id;
private String accountNumber;
private BigDecimal balance;
@Version
private Integer version; // Автоматически управляется JPA
public void withdraw(BigDecimal amount) {
this.balance = balance.subtract(amount);
// version будет автоматически увеличена при save()
}
}
// Service
@Service
public class BankAccountService {
@Autowired
private BankAccountRepository repository;
@Transactional
public void withdrawMoney(Long accountId, BigDecimal amount) {
// Читаем счёт
BankAccount account = repository.findById(accountId)
.orElseThrow(() -> new RuntimeException("Account not found"));
// Проверяем баланс
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("Insufficient funds");
}
// Снимаем деньги
account.withdraw(amount);
// Сохраняем
try {
repository.save(account);
} catch (OptimisticLockingFailureException e) {
// Версия не совпадает - кто-то другой обновил счёт!
throw new RuntimeException("Account was updated by another user, please try again");
}
}
}
// Repository
public interface BankAccountRepository extends JpaRepository<BankAccount, Long> {
}
// Использование
class Example {
public static void main(String[] args) {
// Транзакция 1
new Thread(() -> {
try {
bankAccountService.withdrawMoney(1L, new BigDecimal("100"));
System.out.println("Транзакция 1: успешно");
} catch (RuntimeException e) {
System.out.println("Транзакция 1: ошибка - " + e.getMessage());
}
}).start();
// Транзакция 2 (одновременно)
new Thread(() -> {
try {
bankAccountService.withdrawMoney(1L, new BigDecimal("150"));
System.out.println("Транзакция 2: успешно");
} catch (RuntimeException e) {
System.out.println("Транзакция 2: ошибка - " + e.getMessage());
}
}).start();
}
}
SQL под капотом
-- При чтении
SELECT id, account_number, balance, version FROM bank_accounts WHERE id = 1;
-- Результат: version = 5
-- При сохранении
UPDATE bank_accounts
SET balance = 900.00, version = 6
WHERE id = 1 AND version = 5;
-- Если версия не 5 → 0 строк обновлено → OptimisticLockException
Тип версии
// Все эти типы поддерживаются JPA:
@Version
private int version; // int
@Version
private Integer version; // Integer
@Version
private long version; // long
@Version
private Long version; // Long
@Version
private short version; // short
@Version
private Short version; // Short
@Version
private Timestamp version; // Timestamp (редко)
Сравнение: оптимистичная vs пессимистичная блокировка
// ❌ Пессимистичная блокировка (плохо в большинстве случаев)
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id")
User findByIdWithLock(@Param("id") Long id);
// SELECT ... FOR UPDATE (база блокирует строку)
// Медленнее, но гарантирует эксклюзивный доступ
// ✅ Оптимистичная блокировка (правильный выбор обычно)
// С @Version, никакой явной блокировки на уровне БД
// Быстрее, работает только если конфликты редки
Когда использовать @Version
Используйте @Version когда:
- Конфликты обновления редкие
- Нужна высокая пропускная способность (high throughput)
- Возможна retry логика
- Работаете с веб-приложениями (REST API)
Используйте пессимистичную блокировку когда:
- Конфликты частые
- Нужна абсолютная гарантия актуальности данных
- Работаете с критичными операциями (платежи)
Обработка OptimisticLockException
@Transactional
public void updateWithRetry(Long id, Consumer<Entity> updater) {
int maxRetries = 3;
int retries = 0;
while (retries < maxRetries) {
try {
Entity entity = repository.findById(id).orElseThrow();
updater.accept(entity);
repository.save(entity);
return; // Успех
} catch (OptimisticLockingFailureException e) {
retries++;
if (retries >= maxRetries) {
throw new RuntimeException("Failed after " + maxRetries + " retries", e);
}
// Попытаемся снова
try {
Thread.sleep(100 * retries); // Экспоненциальная задержка
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(ie);
}
}
}
}
Заключение
@Version предоставляет элегантный механизм для обработки одновременных обновлений без пессимистичной блокировки. JPA автоматически управляет версией, увеличивая её при каждом обновлении, и проверяет её при сохранении. Это предотвращает конфликты обновления и гарантирует целостность данных в многопоточной среде.