← Назад к вопросам

Как транзакция защищает от потери данных

1.8 Middle🔥 181 комментариев
#ORM и Hibernate#Spring Boot и Spring Data#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как транзакция защищает от потери данных

Транзакция в базе данных — это последовательность операций, которые либо полностью выполняются, либо полностью откатываются. Это фундаментальный механизм для обеспечения целостности и надежности данных в приложениях.

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 для операций, которые должны быть атомарны.

Как транзакция защищает от потери данных | PrepBro