Как транзакция защищает от потери данных
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как транзакция защищает от потери данных
Транзакция в базе данных — это последовательность операций, которые либо полностью выполняются, либо полностью откатываются. Это фундаментальный механизм для обеспечения целостности и надежности данных в приложениях.
1. ACID свойства транзакций
Транзакции гарантируют четыре ключевых свойства:
1.1 Atomicity (Атомарность)
Операция либо полностью выполняется, либо вообще не выполняется. Нет промежуточных состояний:
// БЕЗ ТРАНЗАКЦИИ: опасно!
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account from = accountRepository.findById(fromAccountId);
from.setBalance(from.getBalance().subtract(amount));
accountRepository.save(from); // Сохранили 1-е изменение
// СБОЙ ДО 2-го сохранения!
int a = 1 / 0; // NullPointerException
Account to = accountRepository.findById(toAccountId);
to.setBalance(to.getBalance().add(amount));
accountRepository.save(to); // Это не выполнится
}
// Результат: деньги уходят, но не приходят - ПОТЕРЯ ДАННЫХ!
// С ТРАНЗАКЦИЕЙ: безопасно!
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account from = accountRepository.findById(fromAccountId);
from.setBalance(from.getBalance().subtract(amount));
accountRepository.save(from);
int a = 1 / 0; // Ошибка
Account to = accountRepository.findById(toAccountId);
to.setBalance(to.getBalance().add(amount));
accountRepository.save(to);
}
// Результат: ВСЕ изменения откатываются, данные не нарушены
1.2 Consistency (Согласованность)
БД переходит из одного согласованного состояния в другое. Никогда нарушаются constraints:
// Constraint: сумма всех счётов в банке должна быть неизменна
// Account.balance >= 0 (не может быть отрицательный баланс)
@Transactional
public void transferMoney(Long from, Long to, BigDecimal amount) {
accountRepository.updateBalance(from, -amount);
accountRepository.updateBalance(to, amount);
// ОБЕ операции выполняются или ОБЕ откатываются
// Никогда: одна выполнена, другая нет
}
1.3 Isolation (Изоляция)
Транзакции не видят незавершённые изменения других транзакций:
// Транзакция 1
@Transactional(isolation = Isolation.READ_COMMITTED)
public void createOrder() {
Order order = new Order();
order.setStatus("PENDING");
orderRepository.save(order);
// Транзакция 2 НЕ видит этот заказ до коммита
// Если Транзакция 1 откатится - Транзакция 2 не будет затронута
}
// Уровни изоляции
// READ_UNCOMMITTED: видит незафиксированные изменения (грязные чтения)
// READ_COMMITTED: видит только зафиксированные данные
// REPEATABLE_READ: блокирует данные от изменений в течение транзакции
// SERIALIZABLE: максимальная изоляция, медленнее всего
1.4 Durability (Долговечность)
Завершённая транзакция сохраняется, даже при сбоях:
@Transactional
public void createUser(User user) {
userRepository.save(user);
// После успешного коммита, данные сохранены на диск
// Даже если сервер упадёт прямо сейчас - данные восстановятся
}
2. Защита от различных типов потерь данных
Потеря 1: Потеря данных из-за исключения
// НЕПРАВИЛЬНО: без транзакции
public void registerUser(User user, Profile profile) {
userRepository.save(user); // Пользователь сохранён
if (profile.isInvalid()) {
throw new IllegalArgumentException("Invalid profile");
// Пользователь остался в БД, но профиль не создан!
}
profileRepository.save(profile);
}
// ПРАВИЛЬНО: с транзакцией
@Transactional
public void registerUser(User user, Profile profile) {
userRepository.save(user);
if (profile.isInvalid()) {
throw new IllegalArgumentException("Invalid profile");
// ВСЕ изменения откатились, пользователь удален из БД
}
profileRepository.save(profile);
}
Потеря 2: Race Condition между двумя запросами
// Без защиты транзакции
Thread 1:
balance = db.query("SELECT balance FROM accounts WHERE id = 1") // 1000
balance -= 500
db.update("UPDATE accounts SET balance = ? WHERE id = 1", 500)
Thread 2 (параллельно):
balance = db.query("SELECT balance FROM accounts WHERE id = 1") // 1000 (видит старое значение!)
balance -= 300
db.update("UPDATE accounts SET balance = ? WHERE id = 1", 700)
// Результат: баланс должен быть 200, но он 700!
// С транзакцией и блокировкой
@Transactional(isolation = Isolation.SERIALIZABLE)
public void withdraw(Long accountId, BigDecimal amount) {
Account account = accountRepository.findByIdForUpdate(accountId);
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
}
// findByIdForUpdate выполняет "SELECT ... FOR UPDATE"
// Вторая транзакция ждёт, пока первая завершится
Потеря 3: Частичные обновления
// Без транзакции: опасно
public void updateUserAndNotifications(Long userId, UserDTO dto) {
User user = userRepository.findById(userId);
user.setName(dto.getName());
user.setEmail(dto.getEmail());
userRepository.save(user); // Сохранили пользователя
// Здесь может быть ошибка!
notificationService.notifyEmailChange(dto.getEmail());
NotificationPreference pref = notificationRepository.findByUserId(userId);
pref.setEmailNotifications(true);
notificationRepository.save(pref); // А это может не выполниться
}
// Результат: пользователь обновлён, но его настройки уведомлений нет!
// С транзакцией: гарантирует всё или ничего
@Transactional
public void updateUserAndNotifications(Long userId, UserDTO dto) {
User user = userRepository.findById(userId);
user.setName(dto.getName());
user.setEmail(dto.getEmail());
userRepository.save(user);
notificationService.notifyEmailChange(dto.getEmail());
NotificationPreference pref = notificationRepository.findByUserId(userId);
pref.setEmailNotifications(true);
notificationRepository.save(pref);
}
// Если что-то упадёт - всё откатится
3. Практический пример: E-commerce заказ
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryRepository inventoryRepository;
@Autowired
private PaymentService paymentService;
@Autowired
private NotificationService notificationService;
// БЕЗ ТРАНЗАКЦИИ: ОПАСНО!
public Order createOrder_UNSAFE(Long userId, List<OrderItem> items) {
// Создаём заказ
Order order = new Order();
order.setUserId(userId);
order.setStatus("PENDING");
orderRepository.save(order);
// Резервируем товары
for (OrderItem item : items) {
Inventory inv = inventoryRepository.findById(item.getProductId());
inv.setQuantity(inv.getQuantity() - item.getQuantity());
inventoryRepository.save(inv); // Товар зарезервирован!
}
// Обрабатываем платёж
Payment payment = paymentService.process(userId, order.getTotalAmount());
// СБОЙ: платёж не прошёл!
// Результат: заказ создан, товары зарезервированы, но оплаты нет!
// Товары потеряны!
return order;
}
// С ТРАНЗАКЦИЕЙ: БЕЗОПАСНО!
@Transactional
public Order createOrder_SAFE(Long userId, List<OrderItem> items) {
// Создаём заказ
Order order = new Order();
order.setUserId(userId);
order.setStatus("PENDING");
orderRepository.save(order);
// Резервируем товары
BigDecimal totalPrice = BigDecimal.ZERO;
for (OrderItem item : items) {
Inventory inv = inventoryRepository.findByIdForUpdate(item.getProductId());
if (inv.getQuantity() < item.getQuantity()) {
throw new OutOfStockException("Product out of stock");
}
inv.setQuantity(inv.getQuantity() - item.getQuantity());
inventoryRepository.save(inv);
totalPrice = totalPrice.add(item.getPrice().multiply(
new BigDecimal(item.getQuantity())));
}
order.setTotalAmount(totalPrice);
orderRepository.save(order);
// Обрабатываем платёж
Payment payment = paymentService.process(userId, totalPrice);
if (!payment.isSuccessful()) {
throw new PaymentFailedException("Payment declined");
}
// Обновляем статус
order.setStatus("CONFIRMED");
orderRepository.save(order);
// Отправляем уведомление
notificationService.notifyOrderCreated(order);
return order;
// Если ЛЮБАЯ операция падает - ВСЁ откатывается:
// - заказ удаляется
// - товары возвращаются на склад
// - платёж не обрабатывается
}
}
4. Уровни транзакционности в Spring
@Transactional
public void defaultBehavior() {
// REQUIRED: использует текущую транзакцию или создаёт новую
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void alwaysNewTransaction() {
// Создаёт НОВУЮ транзакцию, предыдущая приостанавливается
}
@Transactional(propagation = Propagation.NESTED)
public void nestedTransaction() {
// Создаёт SAVEPOINT в текущей транзакции
}
@Transactional(readOnly = true)
public User findUser(Long id) {
// Оптимизация для только чтения
return userRepository.findById(id);
}
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
// Максимальная изоляция, но медленнее
}
@Transactional(rollbackFor = Exception.class)
public void rollbackOnAnyException() {
// Откатываемся на ЛЮБОМ исключении
}
5. Резюме
Транзакции защищают от потери данных через:
- Atomicity — всё или ничего
- Consistency — интегральность constraints
- Isolation — отсутствие race conditions
- Durability — сохранение после коммита
Без транзакций:
- Данные могут остаться в несогласованном состоянии
- Конкурентные запросы могут конфликтовать
- Исключения могут оставить БД в неправильном состоянии
С транзакциями:
- Гарантируется целостность данных
- Автоматический откат при ошибках
- Правильная обработка параллельных запросов
Правило: ВСЕГДА используйте @Transactional для операций, которые должны быть атомарны.